diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8ad02b7162b6a..669395564db44 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,10 +3,13 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -// Looks like 'oss:ciGroup:1' or 'oss:firefoxSmoke' -def JOB_PARTS = params.CI_GROUP.split(':') +def CI_GROUP_PARAM = params.CI_GROUP + +// Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke', or 'all:serverMocha' +def JOB_PARTS = CI_GROUP_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' def JOB = JOB_PARTS[1] +def NEED_BUILD = JOB != 'serverMocha' def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) @@ -31,13 +34,15 @@ stage("Kibana Pipeline") { print "Agent ${agentNumberInside} - ${agentExecutions} executions" kibanaPipeline.withWorkers('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() } }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() } @@ -61,7 +66,17 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { - if (job == 'firefoxSmoke') { + if (job == 'serverMocha') { + return kibanaPipeline.getPostBuildWorker('serverMocha', { + kibanaPipeline.bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/mocha + """, + "run `node scripts/mocha`" + ) + }) + } else if (job == 'firefoxSmoke') { return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) diff --git a/.eslintignore b/.eslintignore index cf13fc28467d9..90155ca9cb681 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ bower_components /plugins /built_assets /html_docs +/src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/fixtures/vislib/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts @@ -19,7 +20,6 @@ bower_components /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana -/packages/kbn-es-query/src/kuery/ast/kuery.js /packages/kbn-pm/dist /packages/kbn-plugin-generator/sao_template/template /packages/kbn-ui-framework/dist diff --git a/.eslintrc.js b/.eslintrc.js index daf49d9d08281..fe546ec02a668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,18 +64,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/core/public/application/**/*.{js,ts,tsx}'], - rules: { - 'react/no-danger': 'off', - }, - }, - { - files: ['src/legacy/core_plugins/console/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/legacy/core_plugins/data/**/*.{js,ts,tsx}'], rules: { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0f1136fd5334b..d567f267afa9d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,9 +40,11 @@ # ML team owns the transform plugin, ES team added here for visibility # because the plugin lives in Kibana's Elasticsearch management section. /x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui +/x-pack/test/functional/apps/transform/ @elastic/ml-ui +/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui +/x-pack/test/functional/services/transform.ts @elastic/ml-ui # Operations -/renovate.json5 @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations @@ -59,6 +61,7 @@ /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform +/packages/kbn-config-schema/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security @@ -71,8 +74,10 @@ /x-pack/test/api_integration/apis/security/ @elastic/kibana-security # Kibana Stack Services +/src/dev/i18n @elastic/kibana-stack-services /packages/kbn-analytics/ @elastic/kibana-stack-services /src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services +/src/plugins/usage_collection/ @elastic/kibana-stack-services /x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services /x-pack/legacy/plugins/alerting @elastic/kibana-stack-services /x-pack/legacy/plugins/actions @elastic/kibana-stack-services diff --git a/.i18nrc.json b/.i18nrc.json index d0d8beb6f5337..e5ba6762da154 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -8,6 +8,7 @@ "embeddableApi": "src/plugins/embeddable", "share": "src/plugins/share", "esUi": "src/plugins/es_ui_shared", + "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", "inputControl": "src/legacy/core_plugins/input_control_vis", "inspector": "src/plugins/inspector", @@ -15,7 +16,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnESQuery": "packages/kbn-es-query", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64a1dd0526d58..e2a8459c2b01a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,13 +399,19 @@ Test runner arguments: - `[test path]` is the relative path to the test file. Examples: - - Run the entire elasticsearch_service test suite with yarn: - `node scripts/jest src/core/server/elasticsearch/elasticsearch_service.test.ts` - - Run the jest test case whose description matches 'stops both admin and data clients': - `node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts` + - Run the entire elasticsearch_service test suite: + ``` + node scripts/jest src/core/server/elasticsearch/elasticsearch_service.test.ts + ``` + - Run the jest test case whose description matches `stops both admin and data clients`: + ``` + node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts + ``` - Run the api integration test case whose description matches the given string: - `node scripts/functional_tests_server --config test/api_integration/config.js` - `node scripts/functional_test_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets'` + ``` + node scripts/functional_tests_server --config test/api_integration/config.js + node scripts/functional_test_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets' + ``` ### Debugging Unit Tests diff --git a/Jenkinsfile b/Jenkinsfile index c002832d4d51a..6030f2b4a021d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 180, unit: 'MINUTES') { + timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { catchError { diff --git a/TYPESCRIPT.md b/TYPESCRIPT.md index 0c28060de40e5..7be9a5e4f3b17 100644 --- a/TYPESCRIPT.md +++ b/TYPESCRIPT.md @@ -21,7 +21,7 @@ The first thing that will probably happen when you convert a `.js` file in our s declare module '@elastic/eui' { // Add your types here - export const EuiPopoverTitle: React.SFC; + export const EuiPopoverTitle: React.FC; ... } ``` @@ -47,13 +47,13 @@ Since `@elastic/eui` already ships with a module declaration, any local addition // file `typings/@elastic/eui/index.d.ts` import { CommonProps } from '@elastic/eui'; -import { SFC } from 'react'; +import { FC } from 'react'; declare module '@elastic/eui' { export type EuiNewComponentProps = CommonProps & { additionalProp: string; }; - export const EuiNewComponent: SFC; + export const EuiNewComponent: FC; } ``` diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index 67ec15892afe4..a00fedf7e7ac4 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -26,7 +26,9 @@ To use the create or update role API, you must have the `manage_security` cluste (Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage. `elasticsearch`:: - (Optional, object) {es} cluster and index privileges. Valid keys include `cluster`, `indices`, and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. + (Optional, object) {es} cluster and index privileges. Valid keys include + `cluster`, `indices`, and `run_as`. For more information, see + {ref}/defining-roles.html[Defining roles]. `kibana`:: (list) Objects that specify the <> for the role: diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 815e731760785..1f064c1cad3fd 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -10,7 +10,7 @@ so it's easy to share a specific query or view with others. In the screenshot below, you can begin to see some of the transaction fields available for filtering on: [role="screenshot"] -image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM UI in Kibana] +image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] [float] ==== Example queries diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 4d2ae5d01688c..6f147d0e3223a 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -2,18 +2,15 @@ [[agent-configuration]] === APM Agent configuration -beta[] APM Agent configuration allows you to fine-tune your agent configuration directly in Kibana. +APM Agent configuration allows you to fine-tune your agent configuration directly in Kibana. Best of all, changes are automatically propagated to your APM agents so there's no need to redeploy. -To get started, simply choose the service and environment you wish to configure. +To get started, simply choose the services and environments you wish to configure. +The APM app will let you know when your configurations have been applied by your agents. [role="screenshot"] image::apm/images/apm-agent-configuration.png[APM Agent configuration in Kibana] -IMPORTANT: As this feature is in Beta, a limited number of configuration settings are supported. -We recommend you watch your agent logs to confirm that configuration has been applied. -If you have feedback, please reach out in our https://discuss.elastic.co/c/apm[Discuss forum]. - [float] ==== Precedence @@ -34,6 +31,26 @@ 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` diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index e80438975cba0..689fa1fffa89e 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -10,12 +10,12 @@ This makes it very easy to quickly see which errors are affecting your services, and to take actions to rectify them. [role="screenshot"] -image::apm/images/apm-errors-overview.png[Example view of the errors overview in the APM UI in Kibana] +image::apm/images/apm-errors-overview.png[Example view of the errors overview in the APM app in Kibana] Selecting an error group ID or error message brings you to the *Error group*. [role="screenshot"] -image::apm/images/apm-error-group.png[Example view of the error group page in the APM UI in Kibana] +image::apm/images/apm-error-group.png[Example view of the error group page in the APM app in Kibana] Here, you'll see the error message, culprit, and the number of occurrences over time. @@ -43,4 +43,4 @@ With Watcher, your team can set up reports within minutes. Watches are managed separately in the dedicated Watcher UI available in Advanced Settings. [role="screenshot"] -image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM UI in Kibana] \ No newline at end of file +image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 8f1265b73a8f4..4a391f1a49672 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -10,7 +10,7 @@ image::apm/images/apm-setup.png[Installation instructions on the APM page in Kib Index patterns tell Kibana which Elasticsearch indices you want to explore. -An APM index pattern is necessary for certain features in the APM UI, like the query bar. +An APM index pattern is necessary for certain features in the APM app, like the query bar. To set up the correct index pattern, simply click *Load Kibana objects* at the bottom of the Setup Instructions. diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png index a75702ad570d0..6a9789b5a6ecd 100644 Binary files a/docs/apm/images/apm-metrics.png and b/docs/apm/images/apm-metrics.png differ diff --git a/docs/apm/images/jvm-metrics.png b/docs/apm/images/jvm-metrics.png new file mode 100644 index 0000000000000..ffeab27e10246 Binary files /dev/null and b/docs/apm/images/jvm-metrics.png differ diff --git a/docs/apm/metrics.asciidoc b/docs/apm/metrics.asciidoc index 3fc63d6a1344a..ab394b785ef84 100644 --- a/docs/apm/metrics.asciidoc +++ b/docs/apm/metrics.asciidoc @@ -2,13 +2,21 @@ === Metrics overview The *Metrics* overview provides agent-specific metrics, -which lets you perform more in-depth root cause analysis investigations within the APM UI. +which lets you perform more in-depth root cause analysis investigations within the APM app. If you're experiencing a problem with your service, you can use this page to attempt to find the underlying cause. For example, you might be able to correlate a high number of errors with a long transaction duration, high CPU usage, or a memory leak. [role="screenshot"] -image::apm/images/apm-metrics.png[Example view of the Metrics overview in APM UI in Kibana] +image::apm/images/apm-metrics.png[Example view of the Metrics overview in APM app in Kibana] + +If you're using the Java Agent, the metrics view focuses on JVMs. +A detailed view of metrics per JVM makes it much easier to analyze the provided metrics: +CPU usage, memory usage, heap or non-heap memory, +thread count, garbage collection rate, and garbage collection time spent per minute. + +[role="screenshot"] +image::apm/images/jvm-metrics.png[Example view of the Metrics overview for the Java Agent] [[machine-learning-integration]] === Machine Learning integration @@ -17,7 +25,7 @@ The Machine Learning integration will initiate a new job predefined to calculate The response time graph will show the expected bounds and annotate the graph when the anomaly score is 75 or above. [role="screenshot"] -image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM UI in Kibana] +image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM app in Kibana] Jobs can be created per transaction type and based on the average response time. You can manage jobs in the *Machine Learning jobs management*. diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index a6620b5cea7b7..9af3e74562dab 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -6,4 +6,4 @@ The *Services* overview gives you quick insights into the health and general per You can add services by setting the `service.name` configuration in each of the {apm-agents-ref}[APM agents] you’re instrumenting. [role="screenshot"] -image::apm/images/apm-services-overview.png[Example view of services table the APM UI in Kibana] \ No newline at end of file +image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index d23c4f5f4caea..75eae61b4cf12 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -9,7 +9,7 @@ The span timeline visualization is a bird's-eye view of what your application wa This makes it useful for visualizing where the selected transaction spent most of its time. [role="screenshot"] -image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM UI in Kibana] +image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] View a span in detail by clicking on it in the timeline waterfall. For example, in the below screenshot we've clicked on an SQL Select database query. @@ -20,13 +20,13 @@ Finally, APM knows which files are your code and which are just modules or libra These library frames will be minimized by default in order to show you the most relevant stack trace. [role="screenshot"] -image::apm/images/apm-span-detail.png[Example view of a span detail in the APM UI in Kibana] +image::apm/images/apm-span-detail.png[Example view of a span detail in the APM app in Kibana] If your span timeline is colorful, it's indicative of a <>. Services in a distributed trace are separated by color and listed in the order they occur. [role="screenshot"] -image::apm/images/apm-services-trace.png[Example of distributed trace colors in the APM UI in Kibana] +image::apm/images/apm-services-trace.png[Example of distributed trace colors in the APM app in Kibana] Don't forget, a distributed trace includes more than one transaction. When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon, @@ -37,4 +37,4 @@ After exploring these traces, you can return to the full trace by clicking *View full trace* in the upper right hand corner of the page. [role="screenshot"] -image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM UI in Kibana] +image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana] diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 214b997818503..09d8f52b92840 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -11,7 +11,7 @@ it's the collective amount of pain a specific endpoint is causing your users. If there's a particular endpoint you're worried about, you can click on it to view the <>. [role="screenshot"] -image::apm/images/apm-traces.png[Example view of the Traces overview in APM UI in Kibana] +image::apm/images/apm-traces.png[Example view of the Traces overview in APM app in Kibana] [float] [[distributed-tracing]] @@ -22,7 +22,7 @@ Distributed tracing is a key feature of modern application performance monitorin service-based architectures. Distributed tracing allows APM users to automatically trace requests all the way through the service architecture, -and visualize those traces in one single view in the APM UI. +and visualize those traces in one single view in the APM app. This is accomplished by tracing all of the requests, from the initial web request to your front-end service, to queries made to your back-end services. This makes finding possible bottlenecks throughout your application much easier and faster. @@ -31,6 +31,6 @@ By definition, a distributed trace includes more than one transaction. You can use the <> to view a waterfall display of all of the transactions from individual services that are connected in a trace. [role="screenshot"] -image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM UI in Kibana] +image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] TIP: Distributed tracing is supported by all APM agents and there’s no additional configuration needed. \ No newline at end of file diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index e70587baa65ea..33f61adc8be63 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -10,9 +10,9 @@ The *Transactions* table, however, provides only a list of _transaction groups_ In other words, this view groups all transactions of the same name together, and only displays one transaction for each group. [role="screenshot"] -image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM UI in Kibana] +image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana] -*Time spent by span type* -- beta[] Certain agents support breakdown graphs in the APM UI. +*Time spent by span type* -- Most agents support breakdown graphs in the APM app. This graph is an easy way to visualize where your application is spending most of its time. For example, is your app spending time in external calls, database processing, or application code execution? @@ -22,8 +22,6 @@ This could be a sign that the agent does not have auto-instrumentation for whate It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction. -TIP: If the *Time spent by span type* chart is missing in the APM UI, it means your agent does not support this feature yet. - *Transaction duration* shows the response times for this service and is broken down into average, 95th, and 99th percentile. If there's a weird spike that you'd like to investigate, you can simply zoom in on the graph - this will adjust the specific time range, diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 07f3cf028dc0e..330cc63d10548 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -28,7 +28,7 @@ Returns `true` if all of the conditions are met. See also <>. *Expression syntax* [source,js] ---- -all {neq “foo”} {neq “bar”} {neq “fizz”} +all {neq "foo"} {neq "bar"} {neq "fizz"} all condition={gt 10} condition={lt 20} ---- @@ -49,7 +49,7 @@ filters } | render ---- -This sets the color of the metric text to `”red”` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`. +This sets the color of the metric text to `"red"` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`. *Accepts:* `null` @@ -76,8 +76,8 @@ Converts between core types, including `string`, `number`, `null`, `boolean`, an *Expression syntax* [source,js] ---- -alterColumn “cost” type=”string” -alterColumn column=”@timestamp” name=”foo” +alterColumn "cost" type="string" +alterColumn column="@timestamp" name="foo" ---- *Code example* @@ -85,7 +85,7 @@ alterColumn column=”@timestamp” name=”foo” ---- filters | demodata -| alterColumn “time” name=”time_in_ms” type=”number” +| alterColumn "time" name="time_in_ms" type="number" | table | render ---- @@ -97,7 +97,7 @@ This renames the `time` column to `time_in_ms` and converts the type of the colu |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `column` |`string` @@ -124,7 +124,7 @@ Returns `true` if at least one of the conditions is met. See also <>. *Expression syntax* [source,js] ---- -any {eq “foo”} {eq “bar”} {eq “fizz”} +any {eq "foo"} {eq "bar"} {eq "fizz"} any condition={lte 10} condition={gt 30} ---- @@ -140,7 +140,7 @@ filters | pie | render ---- -This filters out any rows that don’t contain `“elasticsearch”`, `“kibana”` or `“x-pack”` in the `project` field. +This filters out any rows that don’t contain `"elasticsearch"`, `"kibana"` or `"x-pack"` in the `project` field. *Accepts:* `null` @@ -167,9 +167,9 @@ Creates a `datatable` with a single value. See also <>. *Expression syntax* [source,js] ---- -as -as “foo” -as name=”bar” +as +as "foo" +as name="bar" ---- *Code example* @@ -182,7 +182,7 @@ filters | plot | render ---- -`as` casts any primitive value (`string`, `number`, `date`, `null`) into a `datatable` with a single row and a single column with the given name (or defaults to `"value"` if no name is provided). This is useful when piping a primitive value into a function that only takes `datatable` as an input. +`as` casts any primitive value (`string`, `number`, `date`, `null`) into a `datatable` with a single row and a single column with the given name (or defaults to `"value"` if no name is provided). This is useful when piping a primitive value into a function that only takes `datatable` as an input. In the example above, `ply` expects each `fn` subexpression to return a `datatable` in order to merge the results of each `fn` back into a `datatable`, but using a `math` aggregation in the subexpressions returns a single `math` value, which is then cast into a `datatable` using `as`. @@ -192,7 +192,7 @@ In the example above, `ply` expects each `fn` subexpression to return a `datatab |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `name` |`string` @@ -223,7 +223,7 @@ asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114" image dataurl={asset "asset-c661a7cc-11be-45a1-a401-d7592ea7917a"} mode="contain" | render ---- -The image asset stored with the ID `“asset-c661a7cc-11be-45a1-a401-d7592ea7917a”` is passed into the `dataurl` argument of the `image` function to display the stored asset. +The image asset stored with the ID `"asset-c661a7cc-11be-45a1-a401-d7592ea7917a"` is passed into the `dataurl` argument of the `image` function to display the stored asset. *Accepts:* `null` @@ -251,7 +251,7 @@ Configures the axis of a visualization. Only used with <>. [source,js] ---- axisConfig show=false -axisConfig position=”right” min=0 max=10 tickSize=1 +axisConfig position="right" min=0 max=10 tickSize=1 ---- *Code example* @@ -260,9 +260,9 @@ axisConfig position=”right” min=0 max=10 tickSize=1 filters | demodata | pointseries x="size(cost)" y="project" color="project" -| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} - legend=false - xaxis={axisConfig position="top" min=0 max=400 tickSize=100} +| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} + legend=false + xaxis={axisConfig position="top" min=0 max=400 tickSize=100} yaxis={axisConfig position="right"} | render ---- @@ -276,21 +276,21 @@ This sets the `x-axis` to display on the top of the chart and sets the range of |`max` |`number`, `string`, `null` -|The maximum value displayed in the axis. Must be a `number`, a date in milliseconds since epoch, or an ISO8601 `string` +|The maximum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an ISO8601 string. |`min` |`number`, `string`, `null` -|The minimum value displayed in the axis. Must be a `number`, a date in milliseconds since epoch, or an ISO8601 `string` +|The minimum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an ISO8601 string. |`position` |`string` -|The position of the axis labels. For example, `"top"`, `"bottom"`, `"left"`, or `"right"`. +|The position of the axis labels. For example, `"top"`, `"bottom"`, `"left"`, or `"right"`. Default: `"left"` |`show` |`boolean` -|Show the axis labels? +|Show the axis labels? Default: `true` @@ -309,30 +309,30 @@ Default: `true` [[case_fn]] === `case` -Builds a `case` (including a condition/result) to pass to the <> function. +Builds a <>, including a condition and a result, to pass to the <> function. *Expression syntax* [source,js] ---- -case 0 then=”red” -case when=5 then=”yellow” -case if={lte 50} then=”green” +case 0 then="red" +case when=5 then="yellow" +case if={lte 50} then="green" ---- *Code example* [source,text] ---- math "random()" -| progress shape="gauge" label={formatnumber "0%"} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" +| progress shape="gauge" label={formatnumber "0%"} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" color={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} default="red" - }} + }} valueColor={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} default="red" } | render @@ -345,7 +345,7 @@ This sets the color of the progress indicator and the color of the label to `"gr |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `when` |`any` @@ -378,13 +378,13 @@ Clears the _context_, and returns `null`. [[columns_fn]] === `columns` -Includes or excludes columns from a data table. If you specify both, this will exclude first. +Includes or excludes columns from a `datatable`. When both arguments are specified, the excluded columns will be removed first. *Expression syntax* [source,js] ---- -columns include=”@timestamp, projects, cost” -columns exclude=”username, country, age” +columns include="@timestamp, projects, cost" +columns exclude="username, country, age" ---- *Code example* @@ -404,11 +404,11 @@ This only keeps the `price`, `cost`, `state`, and `project` columns from the `de |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `include` |`string` -|A comma-separated list of column names to keep in the `datatable`. +|A comma-separated list of column names to keep in the `datatable`. |`exclude` |`string` @@ -417,20 +417,18 @@ Alias: `include` *Returns:* `datatable` - + [float] [[compare_fn]] === `compare` -Compares the _context_ to specified value to determine `true` or `false`. -Usually used in combination with <> or <>. This only works with primitive types, -such as `number`, `string`, and `boolean`. See also <>, <>, <>, <>, <>, and <>. +Compares the _context_ to specified value to determine `true` or `false`. Usually used in combination with `<>` or <>. This only works with primitive types, such as `number`, `string`, `boolean`, `null`. See also <>, <>, <>, <>, <>, <> *Expression syntax* [source,js] ---- -compare “neq” to=”elasticsearch” -compare op=”lte” to=100 +compare "neq" to="elasticsearch" +compare op="lte" to=100 ---- *Code example* @@ -438,18 +436,18 @@ compare op=”lte” to=100 ---- filters | demodata -| mapColumn project - fn=${getCell project | - switch +| mapColumn project + fn={getCell project | + switch {case if={compare eq to=kibana} then=kibana} {case if={compare eq to=elasticsearch} then=elasticsearch} default="other" } | pointseries size="size(cost)" color="project" -| pie +| pie | render ---- -This maps all `project` values that aren’t `“kibana”` and `“elasticsearch”` to `“other”`. Alternatively, you can use the individual comparator functions instead of compare. See <>, <>, <>, <>, <>, and <>. +This maps all `project` values that aren’t `"kibana"` and `"elasticsearch"` to `"other"`. Alternatively, you can use the individual comparator functions instead of compare. *Accepts:* `string`, `number`, `boolean`, `null` @@ -457,17 +455,17 @@ This maps all `project` values that aren’t `“kibana”` and `“elasticsearc |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `op` |`string` -|The operator to use in the comparison: `eq` (equal to), `gt` (greater than), `gte` (greater than or equal to), `lt` (less than), `lte` (less than or equal to), `ne` or `neq` (not equal to). +|The operator to use in the comparison: `"eq"` (equal to), `"gt"` (greater than), `"gte"` (greater than or equal to), `"lt"` (less than), `"lte"` (less than or equal to), `"ne"` or `"neq"` (not equal to). Default: `"eq"` -|`to` +|`to` -Alias: `this`, `b` +Aliases: `b`, `this` |`any` |The value compared to the _context_. |=== @@ -484,29 +482,28 @@ Creates an object used for styling an element's container, including background, *Expression syntax* [source,js] ---- -containerStyle backgroundColor=”red”’ -containerStyle borderRadius=”50px” -containerStyle border=”1px solid black” -containerStyle padding=”5px” -containerStyle opacity=”0.5” -containerStyle overflow=”hidden” -containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} - backgroundRepeat="no-repeat" - backgroundSize="cover" +containerStyle backgroundColor="red"’ +containerStyle borderRadius="50px" +containerStyle border="1px solid black" +containerStyle padding="5px" +containerStyle opacity="0.5" +containerStyle overflow="hidden" +containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} + backgroundRepeat="no-repeat" + backgroundSize="cover" ---- *Code example* [source,text] ---- -shape "star" fill="#E61D35" maintainAspect=true -| render - containerStyle={ - containerStyle backgroundColor="#F8D546" - borderRadius="200px" - border="4px solid #05509F" - padding="0px" - opacity="0.9" - overflow="hidden" +shape "star" fill="#E61D35" maintainAspect=true +| render containerStyle={ + containerStyle backgroundColor="#F8D546" + borderRadius="200px" + border="4px solid #05509F" + padding="0px" + opacity="0.9" + overflow="hidden" } ---- @@ -566,8 +563,7 @@ Default: `"hidden"` [[context_fn]] === `context` -Returns whatever you pass into it. This can be useful when you need to use the -_context_ as an argument to a function as a sub-expression. +Returns whatever you pass into it. This can be useful when you need to use _context_ as argument to a function as a sub-expression. *Expression syntax* [source,js] @@ -580,40 +576,41 @@ context ---- date | formatdate "LLLL" -| markdown "Last updated: " {context} +| markdown "Last updated: " {context} | render ---- Using the `context` function allows us to pass the output, or _context_, of the previous function as a value to an argument in the next function. Here we get the formatted date string from the previous function and pass it as `content` for the markdown element. *Accepts:* `any` -*Returns:* Original _context_ +*Returns:* Depends on your input and arguments + [float] [[csv_fn]] -=== `csv` +=== `csv` Creates a `datatable` from CSV input. *Expression syntax* [source,js] ---- -csv “fruit, stock - kiwi, 10 - Banana, 5” +csv "fruit, stock + kiwi, 10 + Banana, 5" ---- *Code example* [source,text] ---- csv "fruit,stock - kiwi,10 - banana,5" + kiwi,10 + banana,5" | pointseries color=fruit size=stock | pie | render ---- -This is useful for quickly mocking data. +This creates a `datatable` with `fruit` and `stock` columns with two rows. This is useful for quickly mocking data. *Accepts:* `null` @@ -621,7 +618,7 @@ This is useful for quickly mocking data. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `data` |`string` @@ -631,14 +628,13 @@ Alias: `data` |`string` |The data separation character. -|`newLine` +|`newline` |`string` |The row separation character. |=== *Returns:* `datatable` - [float] [[d_fns]] == D @@ -647,15 +643,15 @@ Alias: `data` [[date_fn]] === `date` -Returns the current time, or a time parsed from a `string`, as milliseconds since epoch. +Returns the current time, or a time parsed from a specified string, as milliseconds since epoch. *Expression syntax* [source,js] ---- date date value=1558735195 -date “2019-05-24T21:59:55+0000” -date “01/31/2019” format=”MM/DD/YYYY” +date "2019-05-24T21:59:55+0000" +date "01/31/2019" format="MM/DD/YYYY" ---- *Code example* @@ -663,11 +659,11 @@ date “01/31/2019” format=”MM/DD/YYYY” ---- date | formatdate "LLL" -| markdown {context} - font={font family="Arial, sans-serif" size=30 align="left" - color="#000000" - weight="normal" - underline=false +| markdown {context} + font={font family="Arial, sans-serif" size=30 align="left" + color="#000000" + weight="normal" + underline=false italic=false} | render ---- @@ -679,17 +675,15 @@ Using `date` without passing any arguments will return the current date and time |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `value` |`string` -|A date string to be parsed into milliseconds since epoch. The date string be either a valid JavaScript Date input or a string to parse -using the `format` argument. Must be an ISO8601 string or you must provide the format. +|An optional date string that is parsed into milliseconds since epoch. The date string can be either a valid JavaScript `Date` input or a string to parse using the `format` argument. Must be an ISO8601 string, or you must provide the format. |`format` |`string` -|The MomentJS format for parsing the optional date -`string`. See the https://momentjs.com/docs/#/displaying/[MomentJS documentation]. +|The MomentJS format used to parse the specified date string. For more information, see https://momentjs.com/docs/#/displaying/. |=== *Returns:* `number` @@ -701,35 +695,17 @@ using the `format` argument. Must be an ISO8601 string or you must provide the f A mock data set that includes project CI times with usernames, countries, and run phases. -*Expression syntax* -[source,js] ----- -demodata -demodata “ci” -demodata type=”shirts” ----- - -*Code example* -[source,text] ----- -filters -| demodata -| table -| render ----- -`demodata` is a mock data set that you can use to start playing around in Canvas. - *Accepts:* `filter` [cols="3*^<"] |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `type` |`string` -|The name of the demo data set to use. +|The name of the demo data set to use. Default: `"ci"` |=== @@ -741,24 +717,7 @@ Default: `"ci"` [[do_fn]] === `do` -Executes multiple sub-expressions, then returns the original _context_. Use for running functions that produce an action or side effect without changing the original _context_. - -*Expression syntax* -[source,js] ----- -do fn={something cool} ----- - -*Code example* -[source,text] ----- -filters -| demodata -| do fn={something cool} -| table -| render ----- -`do` should be used to invoke a function that produces as a side effect without changing the `context`. +Executes multiple sub-expressions, then returns the original _context_. Use for running functions that produce an action or a side effect without changing the original _context_. *Accepts:* `any` @@ -768,12 +727,12 @@ filters |_Unnamed_ † -Aliases: `expression`, `exp`, `fn`, `function` +Aliases: `exp`, `expression`, `fn`, `function` |`any` -|The sub-expressions to execute. The return values of these sub-expressions are not available in the root pipeline as this function simply returns the _context_. +|The sub-expressions to execute. The return values of these sub-expressions are not available in the root pipeline as this function simply returns the original _context_. |=== -*Returns:* Original _context_ +*Returns:* Depends on your input and arguments [float] @@ -793,7 +752,7 @@ dropdownControl valueColumn=agent filterColumn=agent.keyword filterGroup=group1 [source,text] ---- demodata -| dropdownControl valueColumn=project filterColumn=project +| dropdownControl valueColumn=project filterColumn=project | render ---- This creates a dropdown filter element. It requires a data source and uses the unique values from the given `valueColumn` (i.e. `project`) and applies the filter to the `project` column. Note: `filterColumn` should point to a keyword type field for Elasticsearch data sources. @@ -808,18 +767,17 @@ This creates a dropdown filter element. It requires a data source and uses the u |`string` |The column or field that you want to filter. -|`valueColumn` *** -|`string` -|The column or field to extract the unique values for the drop-down control. - |`filterGroup` |`string` |The group name for the filter. + +|`valueColumn` *** +|`string` +|The column or field from which to extract the unique values for the dropdown control. |=== *Returns:* `render` - [float] [[e_fns]] == E @@ -836,7 +794,7 @@ Returns whether the _context_ is equal to the argument. eq true eq null eq 10 -eq “foo” +eq "foo" ---- *Code example* @@ -844,18 +802,18 @@ eq “foo” ---- filters | demodata -| mapColumn project - fn=${getCell project | - switch +| mapColumn project + fn={getCell project | + switch {case if={eq kibana} then=kibana} {case if={eq elasticsearch} then=elasticsearch} default="other" } | pointseries size="size(cost)" color="project" -| pie +| pie | render ---- -This changes all values in the project column that don’t equal `“kibana”` or `“elasticsearch”` to `“other”`. +This changes all values in the project column that don’t equal `"kibana"` or `"elasticsearch"` to `"other"`. *Accepts:* `boolean`, `number`, `string`, `null` @@ -863,7 +821,7 @@ This changes all values in the project column that don’t equal `“kibana”` |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`boolean`, `number`, `string`, `null` @@ -877,29 +835,7 @@ Alias: `value` [[escount_fn]] === `escount` -Queries {es} for the number of hits matching the specified query. - -*Expression syntax* -[source,js] ----- -escount index=”logstash-*” -escount "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” -escount query="response:404" index=”kibana_sample_data_logs” ----- - -*Code example* -[source,text] ----- -filters -| escount "Cancelled:true" index="kibana_sample_data_flights" -| math "value" -| progress shape="semicircle" - label={formatnumber 0,0} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} - max={filters | escount index="kibana_sample_data_flights"} -| render ----- -The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights. +Query Elasticsearch for the number of hits matching the specified query. *Accepts:* `filter` @@ -907,9 +843,9 @@ The first `escount` expression retrieves the number of flights that were cancell |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ -Alias: `q`, `query` +Aliases: `q`, `query` |`string` |A Lucene query string. @@ -929,36 +865,7 @@ Default: `_all` [[esdocs_fn]] === `esdocs` -Queries {es} for raw documents. Specify the fields you want to retrieve, -especially if you are asking for a lot of rows. - -*Expression syntax* -[source,js] ----- -esdocs index=”logstash-*” -esdocs "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” -esdocs query="response:404" index=”kibana_sample_data_logs” -esdocs index=”kibana_sample_data_flights” count=100 -esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc" ----- - -*Code example* -[source,text] ----- -filters -| esdocs index="kibana_sample_data_ecommerce" - fields="customer_gender, taxful_total_price, order_date" - sort="order_date, asc" - count=10000 -| mapColumn "order_date" - fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} -| alterColumn "order_date" type="date" -| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" -| plot defaultStyle={seriesStyle lines=3} - palette={palette "#7ECAE3" "#003A4D" gradient=true} -| render ----- -This retrieves the latest 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields. +Query Elasticsearch for raw documents. Specify the fields you want to retrieve, especially if you are asking for a lot of rows. *Accepts:* `filter` @@ -966,7 +873,7 @@ This retrieves the latest 10000 documents data from the `kibana_sample_data_ecom |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `q`, `query` |`string` @@ -986,9 +893,13 @@ Default: `1000` |`index` |`string` -|An index or index pattern. For example, `"logstash-*"`. +|An index or index pattern. For example, `"logstash-*"`. -Default: `"_all"` +Default: `_all` + +|`metaFields` +|`string` +|Comma separated list of meta fields. For example, `"_index,_type"`. |`sort` |`string` @@ -1002,22 +913,7 @@ Default: `"_all"` [[essql_fn]] === `essql` -Queries {es} using {es} SQL. - -*Expression syntax* -[source,js] ----- -essql query=”SELECT * FROM \”logstash*\”” -essql “SELECT * FROM \”apm*\”” count=10000 ----- - -*Code example* -[source,text] ----- -filters -| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" ----- -This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the “kibana_sample_data_flights” index. +Queries Elasticsearch using Elasticsearch SQL. *Accepts:* `filter` @@ -1025,19 +921,21 @@ This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ -Alias: `q`, `query` +Aliases: `q`, `query` |`string` -|An {es} SQL query. +|An Elasticsearch SQL query. |`count` |`number` -|The number of documents to retrieve. Smaller numbers perform better. +|The number of documents to retrieve. For better performance, use a smaller data set. Default: `1000` |`timezone` + +Alias: `tz` |`string` |The timezone to use for date operations. Valid ISO8601 formats and UTC offsets both work. @@ -1056,9 +954,9 @@ Creates a filter that matches a given column to an exact value. *Expression syntax* [source,js] ---- -exactly “state” value=”running” -exactly “age” value=50 filterGroup=”group2” -exactly column=“project” value=”beats” +exactly "state" value="running" +exactly "age" value=50 filterGroup="group2" +exactly column="project" value="beats" ---- *Code example* @@ -1071,7 +969,7 @@ filters | plot defaultStyle={seriesStyle bars=1} | render ---- -The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `”elasticsearch”` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad. +The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `"elasticsearch"` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad. *Accepts:* `filter` @@ -1081,25 +979,23 @@ The `exactly` filter here is added to existing filters retrieved by the `filters |`column` *** -Aliases: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. +|`filterGroup` +|`string` +|The group name for the filter. + |`value` *** Aliases: `v`, `val` |`string` -|The value to match exactly, including white space and -capitalization. - -|`filterGroup` -|`string` -|The group name for the filter. +|The value to match exactly, including white space and capitalization. |=== *Returns:* `filter` - [float] [[f_fns]] == F @@ -1113,8 +1009,8 @@ Filters rows in a `datatable` based on the return value of a sub-expression. *Expression syntax* [source,js] ---- -filterrows {getCell “project” | eq “kibana”} -filterrows fn={getCell “age” | gt 50} +filterrows {getCell "project" | eq "kibana"} +filterrows fn={getCell "age" | gt 50} ---- *Code example* @@ -1123,11 +1019,11 @@ filterrows fn={getCell “age” | gt 50} filters | demodata | filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} -| mapColumn "@timestamp" +| mapColumn "@timestamp" fn={getCell "@timestamp" | rounddate "YYYY-MM"} | alterColumn "@timestamp" type="date" | pointseries x="@timestamp" y="mean(cost)" color="country" -| plot defaultStyle={seriesStyle points="2" lines="1"} +| plot defaultStyle={seriesStyle points="2" lines="1"} palette={palette "#01A4A4" "#CC6666" "#D0D102" "#616161" "#00A1CB" "#32742C" "#F18D05" "#113F8C" "#61AE24" "#D70060" gradient=false} | render ---- @@ -1139,13 +1035,11 @@ This uses `filterrows` to only keep data from India (`IN`), the United States (` |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Aliases: `fn`, `exp`, `expression` +Aliases: `exp`, `expression`, `fn`, `function` |`boolean` -|An expression to pass into each row in the `datatable.` -The expression should return a `boolean`. A `true` value preserves the row, -and a `false` value removes it. +|An expression to pass into each row in the `datatable`. The expression should return a `boolean`. A `true` value preserves the row, and a `false` value removes it. |=== *Returns:* `datatable` @@ -1161,8 +1055,8 @@ Aggregates element filters from the workpad for use elsewhere, usually a data so [source,js] ---- filters -filters group=”timefilter1” -filters group=”timefilter2” group=”dropdownfilter1” ungrouped=true +filters group="timefilter1" +filters group="timefilter2" group="dropdownfilter1" ungrouped=true ---- *Code example* @@ -1171,19 +1065,19 @@ filters group=”timefilter2” group=”dropdownfilter1” ungrouped=true filters group=group2 ungrouped=true | demodata | pointseries x="project" y="size(cost)" color="project" -| plot defaultStyle={seriesStyle bars=0.75} legend=false +| plot defaultStyle={seriesStyle bars=0.75} legend=false font={ - font size=14 - family="'Open Sans', Helvetica, Arial, sans-serif" - align="left" - color="#FFFFFF" - weight="lighter" - underline=true + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true italic=true } | render ---- -`filters` sets the existing filters as context and accepts `group` parameter to create filter groups. +`filters` sets the existing filters as context and accepts a `group` parameter to opt into specific filter groups. Setting `ungrouped` to `true` opts out of using global filters. *Accepts:* `null` @@ -1191,7 +1085,7 @@ filters group=group2 ungrouped=true |=== |Argument |Type |Description -|_Unnamed_ † +|_Unnamed_ † Alias: `group` |`string` @@ -1199,7 +1093,7 @@ Alias: `group` |`ungrouped` -Alias: `nogroup`, `nogroups` +Aliases: `nogroup`, `nogroups` |`boolean` |Exclude filters that belong to a filter group? @@ -1234,14 +1128,14 @@ font lHeight=32 filters | demodata | pointseries x="project" y="size(cost)" color="project" -| plot defaultStyle={seriesStyle bars=0.75} legend=false +| plot defaultStyle={seriesStyle bars=0.75} legend=false font={ - font size=14 - family="'Open Sans', Helvetica, Arial, sans-serif" - align="left" - color="#FFFFFF" - weight="lighter" - underline=true + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true italic=true } | render @@ -1255,7 +1149,7 @@ filters |`align` |`string` -|The horizontal alignment of text. +|The horizontal text alignment. Default: `left` @@ -1265,40 +1159,41 @@ Default: `left` |`family` |`string` -|An acceptable CSS web font string. +|An acceptable CSS web font string Default: `"'Open Sans', Helvetica, Arial, sans-serif"` |`italic` |`boolean` -|Italicize the text? +|Italicize the text? Default: `false` -|`lHeight` +|`lHeight` Alias: `lineHeight` |`number`, `null` -|The line height in pixels. +|The line height in pixels + +Default: `null` |`size` |`number` -|The font size in pixels. +|The font size in pixels Default: `14` |`underline` |`boolean` -|Underline the text? +|Underline the text? Default: `false` |`weight` |`string` -|The font weight. For example, `"normal"`, `"bold"`, `"bolder"`, `"lighter"`, `"100"`, `"200"`, `"300"`, `"400"`, `"500"`, `"600"`, `"700"`, `"800"`, or `"900"`. - -Default: `"normal"` +|The font weight. For example, `"normal"`, `"bold"`, `"bolder"`, `"lighter"`, `"100"`, `"200"`, `"300"`, `"400"`, `"500"`, `"600"`, `"700"`, `"800"`, or `"900"`. +Default: `normal` |=== *Returns:* `style` @@ -1313,8 +1208,8 @@ Formats an ISO8601 date string or a date in milliseconds since epoch using Momen *Expression syntax* [source,js] ---- -formatdate format=”YYYY-MM-DD” -formatdate “MM/DD/YYYY” +formatdate format="YYYY-MM-DD" +formatdate "MM/DD/YYYY" ---- *Code example* @@ -1327,7 +1222,7 @@ filters | plot defaultStyle={seriesStyle points=5} | render ---- -This transforms the dates in the `time` field into strings that look like `“Jan ‘19”`, `“Feb ‘19”`, etc. using a MomentJS format. +This transforms the dates in the `time` field into strings that look like `"Jan ‘19"`, `"Feb ‘19"`, etc. using a MomentJS format. *Accepts:* `number`, `string` @@ -1335,7 +1230,7 @@ This transforms the dates in the `time` field into strings that look like `“Ja |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `format` |`string` @@ -1349,13 +1244,13 @@ Alias: `format` [[formatnumber_fn]] === `formatnumber` -Formats a `number` into a formatted `string` using NumeralJS. See http://numeraljs.com/#format. +Formats a number into a formatted number string using NumeralJS. For more information, see http://numeraljs.com/#format. *Expression syntax* [source,js] ---- -formatnumber format=”$0,0.00” -formatnumber “0.0a” +formatnumber format="$0,0.00" +formatnumber "0.0a" ---- *Code example* @@ -1364,8 +1259,8 @@ formatnumber “0.0a” filters | demodata | math "mean(percent_uptime)" -| progress shape="gauge" - label={formatnumber "0%"} +| progress shape="gauge" + label={formatnumber "0%"} font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align="center"} | render ---- @@ -1377,7 +1272,7 @@ The `formatnumber` subexpression receives the same `context` as the `progress` f |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `format` |`string` @@ -1386,7 +1281,6 @@ Alias: `format` *Returns:* `string` - [float] [[g_fns]] == G @@ -1403,13 +1297,13 @@ Fetches a single cell from a `datatable`. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ -Aliases: `column`, `c` +Aliases: `c`, `column` |`string` |The name of the column to fetch the value from. If not provided, the value is retrieved from the first column. -|`row` +|`row` Alias: `r` |`number` @@ -1418,7 +1312,7 @@ Alias: `r` Default: `0` |=== -*Returns:* Depends on the data in the cell +*Returns:* Depends on your input and arguments [float] @@ -1433,7 +1327,7 @@ Returns whether the _context_ is greater than the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1455,7 +1349,7 @@ Returns whether the _context_ is greater or equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1464,7 +1358,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[h_fns]] == H @@ -1481,7 +1374,7 @@ Retrieves the first N rows from the `datatable`. See also <>. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `count` |`number` @@ -1492,7 +1385,6 @@ Default: `1` *Returns:* `datatable` - [float] [[i_fns]] == I @@ -1509,22 +1401,22 @@ Performs conditional logic. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ *** Alias: `condition` |`boolean` |A `true` or `false` indicating whether a condition is met, usually returned by a sub-expression. When unspecified, the original _context_ is returned. -|`then` -|`any` -|The return value when the condition is `true`. When unspecified and the condition is met, the original _context_ is returned. - |`else` |`any` |The return value when the condition is `false`. When unspecified and the condition is not met, the original _context_ is returned. + +|`then` +|`any` +|The return value when the condition is `true`. When unspecified and the condition is met, the original _context_ is returned. |=== -*Returns:* Depends on your _context_ and arguments +*Returns:* Depends on your input and arguments [float] @@ -1539,37 +1431,31 @@ Displays an image. Provide an image asset as a `base64` data URL, or pass in a s |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `dataurl`, `url` |`string`, `null` |The HTTP(S) URL or `base64` data URL of an image. +Example value for the _Unnamed_ argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+ +------------ + |`mode` |`string` -|`"contain"` shows the entire image, scaled to fit. -`"cover"` fills the container with the image, cropping from the sides or bottom as needed. -`"stretch"` resizes the height and width of the image to 100% of the container. - +|`"contain"` shows the entire image, scaled to fit. `"cover"` fills the container with the image, cropping from the sides or bottom as needed. `"stretch"` resizes the height and width of the image to 100% of the container. Default: `"contain"` |=== -Example value for the `dataurl` argument, formatted as a `base64` data URL: -[source, url] -------------- -data:image/svg+xml;`base64`,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+ -------------- - *Returns:* `image` - [float] [[j_fns]] == J -[float] - [float] [[joinRows_fn]] === `joinRows` @@ -1582,47 +1468,44 @@ Concatenates values from rows in a `datatable` into a single string. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `column` |`string` -|The column or field from which to extract the values. +|The column or field from which to extract the values. -|distinct +|`distinct` |`boolean` -|Extract only unique values? +|Extract only unique values? Default: `true` -|quote +|`quote` |`string` -|The quote character to wrap around each extracted value. +|The quote character to wrap around each extracted value. Default: `"'"` -|separator +|`separator` -Aliases: `sep`, `delimiter` +Aliases: `delimiter`, `sep` |`string` |The delimiter to insert between each extracted value. -Default: `", "` +Default: `","` |=== *Returns:* `string` - [float] [[l_fns]] == L -[float] [float] [[location_fn]] === `location` -Find your current location using the Geolocation API of the browser. Performance can vary, but is fairly accurate. -See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation. +Find your current location using the Geolocation API of the browser. Performance can vary, but is fairly accurate. See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation. Don’t use <> if you plan to generate PDFs as this function requires user input. *Accepts:* `null` @@ -1641,7 +1524,7 @@ Returns whether the _context_ is less than the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1663,7 +1546,7 @@ Returns whether the _context_ is less than or equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1672,7 +1555,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[m_fns]] == M @@ -1681,7 +1563,7 @@ Alias: `value` [[mapColumn_fn]] === `mapColumn` -Adds a column calculated as the result of other columns. Changes are made only when you provide arguments. See also <> and <>. +Adds a column calculated as the result of other columns. Changes are made only when you provide arguments.See also <> and <>. *Accepts:* `datatable` @@ -1689,15 +1571,15 @@ Adds a column calculated as the result of other columns. Changes are made only w |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Alias: `column` +Aliases: `column`, `name` |`string` |The name of the resulting column. |`expression` *** -Alias: `exp`, `fn`, `function` +Aliases: `exp`, `fn`, `function` |`boolean`, `number`, `string`, `null` |A Canvas expression that is passed to each row as a single row `datatable`. |=== @@ -1719,17 +1601,17 @@ Adds an element that renders Markdown text. TIP: Use the <> functio |_Unnamed_ † -Alias: `expression`, `content` +Aliases: `content`, `expression` |`string` -|A string of text that contains Markdown. To concatenate, pass the <> function multiple times. +|A string of text that contains Markdown. To concatenate, pass the `string` function multiple times. Default: `""` |`font` |`style` -|The CSS font properties for the content. For example, `font-family` or `font-weight`. +|The CSS font properties for the content. For example, "font-family" or "font-weight". -Default: `{font}` +Default: `${font}` |=== *Returns:* `render` @@ -1739,7 +1621,7 @@ Default: `{font}` [[math_fn]] === `math` -Interprets a `TinyMath` math expression using a `number` or `datatable` as _context_. The `datatable` columns are available by their column name. If the _context_ is a `number`, it is available as `value`. +Interprets a `TinyMath` math expression using a `number` or `datatable` as _context_. The `datatable` columns are available by their column name. If the _context_ is a number it is available as `value`. *Accepts:* `number`, `datatable` @@ -1747,11 +1629,11 @@ Interprets a `TinyMath` math expression using a `number` or `datatable` as _cont |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ Alias: `expression` |`string` -|An evaluated TinyMath expression. See <>. +|An evaluated `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html. |=== *Returns:* `number` @@ -1771,7 +1653,7 @@ Displays a number over a label. |_Unnamed_ -Aliases: `label`, `text`, `description` +Aliases: `description`, `label`, `text` |`string` |The text describing the metric. @@ -1781,13 +1663,13 @@ Default: `""` |`style` |The CSS font properties for the label. For example, `font-family` or `font-weight`. -Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. +Default: `${font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` |`metricFont` |`style` |The CSS font properties for the metric. For example, `font-family` or `font-weight`. -Default: `{font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}`. +Default: `${font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}` |`metricFormat` @@ -1798,7 +1680,6 @@ Alias: `format` *Returns:* `render` - [float] [[n_fns]] == N @@ -1815,7 +1696,7 @@ Returns whether the _context_ is not equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`boolean`, `number`, `string`, `null` @@ -1824,7 +1705,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[p_fns]] == P @@ -1841,7 +1721,7 @@ Creates a color palette. |=== |Argument |Type |Description -|_Unnamed_ *** † +|_Unnamed_ † Alias: `color` |`string` @@ -1849,7 +1729,7 @@ Alias: `color` |`gradient` |`boolean` -|Make a gradient where supported? +|Make a gradient palette where supported? Default: `false` @@ -1879,17 +1759,17 @@ Configures a pie chart element. |`style` |The CSS font properties for the labels. For example, `font-family` or `font-weight`. -Default: `{font}` +Default: `${font}` |`hole` |`number` -|Draws a hole in the pie, 0-100, as a percentage of the pie radius. +|Draws a hole in the pie, between `0` and `100`, as a percentage of the pie radius. Default: `0` |`labelRadius` |`number` -|The percentage of the container area to use as a radius for the label circle. +|The percentage of the container area to use as a radius for the label circle. Default: `100` @@ -1901,19 +1781,19 @@ Default: `true` |`legend` |`string`, `boolean` -|The legend position. For example, `"nw"`, `"sw"`, `"ne"`, `"se"`. When `false`, the legend is hidden. +|The legend position. For example, `"nw"`, `"sw"`, `"ne"`, `"se"`, or `false`. When `false`, the legend is hidden. Default: `false` |`palette` |`palette` -|A `palette` object for describing the colors to use in this pie chart +|A `palette` object for describing the colors to use in this pie chart. -Default: `{palette}` +Default: `${palette}` |`radius` |`string`, `number` -|The radius of the pie as a percentage (between 0 and 1) of the available space. To automatically set radius, use `"auto"`. +|The radius of the pie as a percentage, between `0` and `1`, of the available space. To automatically set the radius, use `"auto"`. Default: `"auto"` @@ -1923,7 +1803,7 @@ Default: `"auto"` |`tilt` |`number` -|The percentage of tilt, where 1 is fully vertical, and 0 is completely flat. +|The percentage of tilt where `1` is fully vertical, and `0` is completely flat. Default: `1` |=== @@ -1935,7 +1815,7 @@ Default: `1` [[plot_fn]] === `plot` -Configures a plot element. +Configures a chart element. *Accepts:* `pointseries` @@ -1947,13 +1827,13 @@ Configures a plot element. |`seriesStyle` |The default style to use for every series. -Default: `{seriesStyle points=5}` +Default: `${seriesStyle points=5}` |`font` |`style` |The CSS font properties for the labels. For example, `font-family` or `font-weight`. -Default: `{font}` +Default: `${font}` |`legend` |`string`, `boolean` @@ -1963,9 +1843,9 @@ Default: `"ne"` |`palette` |`palette` -|A `palette` object for describing the colors to use in this chart +|A `palette` object for describing the colors to use in this chart. -Default: `{palette}` +Default: `${palette}` |`seriesStyle` † |`seriesStyle` @@ -1999,15 +1879,15 @@ Subdivides a `datatable` by the unique values of the specified columns, and pass |=== |Argument |Type |Description -|`by` *** † +|`by` † |`string` |The column to subdivide the `datatable`. -|`expression` *** † +|`expression` † -Alias: `fn`, `exp`, `function` +Aliases: `exp`, `fn`, `function` |`datatable` -|An expression to pass into each resulting data table. Expressions must return a `datatable`. Use `as` to turn literals into `datatable`s. Multiple expressions must return the same number of rows. If you need to return a different row count, pipe into another instance of <>. If multiple expressions return `datatable`s with the same column names, the last one wins. +|An expression to pass each resulting `datatable` into. Tips: Expressions must return a `datatable`. Use <> to turn literals into `datatable`s. Multiple expressions must return the same number of rows.If you need to return a different row count, pipe into another instance of <>. If multiple expressions returns the columns with the same name, the last one wins. |=== *Returns:* `datatable` @@ -2017,8 +1897,7 @@ Alias: `fn`, `exp`, `function` [[pointseries_fn]] === `pointseries` -Turns a `datatable` into a point series model. Currently we differentiate measure from dimensions by looking for a <>. If you enter a TinyMath -expression in your argument, Canvas treats that argument as a measure. Otherwise, it is a dimension. Dimensions are combined to create unique keys. Measures are then deduplicated by those keys using the specified TinyMath function. +Turn a `datatable` into a point series model. Currently we differentiate measure from dimensions by looking for a `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html. If you enter a `TinyMath` expression in your argument, we treat that argument as a measure, otherwise it is a dimension. Dimensions are combined to create unique keys. Measures are then deduplicated by those keys using the specified `TinyMath` function *Accepts:* `datatable` @@ -2054,7 +1933,7 @@ expression in your argument, Canvas treats that argument as a measure. Otherwise [[progress_fn]] === `progress` -Configures a progress element +Configures a progress element. *Accepts:* `number` @@ -2062,7 +1941,7 @@ Configures a progress element |=== |Argument |Type |Description -| _Unnamed_ +|_Unnamed_ Alias: `shape` |`string` @@ -2086,14 +1965,14 @@ Default: `20` |`style` |The CSS font properties for the label. For example, `font-family` or `font-weight`. -Default: `{font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` +Default: `${font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` |`label` |`boolean`, `string` -|To show or hide the value, use `true` or `false`. Alternatively, provide a string to display as a label. +|To show or hide the label, use `true` or `false`. Alternatively, provide a string to display as a label. Default: `true` - + |`max` |`number` |The maximum value of the progress element. @@ -2115,7 +1994,6 @@ Default: `20` *Returns:* `render` - [float] [[r_fns]] == R @@ -2134,19 +2012,19 @@ Renders the _context_ as a specific element and sets element level options, such |`as` |`string` -|The element type to render. You might want to use a specialized function instead, such as <> or <>. - -|`css` -|`string` -|Any block of custom CSS to be scoped to the element - -Default: `".canvasRenderEl{\n\n}"` +|The element type to render. You probably want a specialized function instead, such as <> or <>. |`containerStyle` |`containerStyle` |The style for the container, including background, border, and opacity. -Default: `{containerStyle}` +Default: `${containerStyle}` + +|`css` +|`string` +|Any block of custom CSS to be scoped to the element. + +Default: `".canvasRenderEl${}"` |=== *Returns:* `render` @@ -2165,20 +2043,26 @@ Configures a repeating image element. |Argument |Type |Description |`emptyImage` -|`string` +|`string`, `null` |Fills the difference between the _context_ and `max` parameter for the element with this image. Provide an image asset as a `base64` data URL, or pass in a sub-expression. Default: `null` |`image` -|`string` +|`string`, `null` |The image to repeat. Provide an image asset as a `base64` data URL, or pass in a sub-expression. +Example value for the `image` argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E +------------ + |`max` |`number` |The maximum number of times the image can repeat. -Default: `100` +Default: `1000` |`size` |`number` @@ -2187,13 +2071,6 @@ Default: `100` Default: `100` |=== - -Example value for the `image` argument, formatted as a `base64` data URL: -[source, url] ------------- -data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E ------------- - *Returns:* `render` @@ -2209,27 +2086,23 @@ Uses a regular expression to replace parts of a string. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ -Alias: `pattern`, `regex` +Aliases: `pattern`, `regex` |`string` -|The text or pattern of a JavaScript regular expression. For example, `"[aeiou]"`. -You can use capturing groups here. +|The text or pattern of a JavaScript regular expression. For example, `"[aeiou]"`. You can use capturing groups here. -|`flags` +|`flags` Alias: `modifiers` -|`datatable` -|Specify flags. See the -https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp[RegExp documentation] -for reference +|`string` +|Specify flags. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp. Default: `"g"` |`replacement` |`string` -|The replacement for the matching parts of `string`. Capturing groups can be accessed -by their index. For example, `$1`. +|The replacement for the matching parts of string. Capturing groups can be accessed by their index. For example, `"$1"`. Default: `""` |=== @@ -2249,28 +2122,29 @@ Configures an image reveal element. |=== |Argument |Type |Description +|`emptyImage` +|`string`, `null` +|An optional background image to reveal over. Provide an image asset as a ``base64`` data URL, or pass in a sub-expression. + +Default: `null` + |`image` -|`string` +|`string`, `null` |The image to reveal. Provide an image asset as a `base64` data URL, or pass in a sub-expression. -|`emptyImage` -|`string` -|An optional background image to reveal over. Provide an image asset as a `base64` data URL, or pass in a sub-expression. +Example value for the `image` argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E +------------ |`origin` |`string` -|The position to start the image fill. For example, `"top"`, `"left"`, `"bottom"`, or `"right"` +|The position to start the image fill. For example, `"top"`, `"bottom"`, `"left"`, or right. Default: `"bottom"` |=== - -Example value for the `image` argument, formatted as a `base64` data URL: -[source, url] ------------- -data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E ------------- - *Returns:* `render` @@ -2286,11 +2160,11 @@ Uses a MomentJS formatting string to round milliseconds since epoch, and returns |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `format` |`string` -|The MomentJS Format to use for bucketing. For example, `"YYYY-MM"` rounds to months. See https://momentjs.com/docs/#/displaying/. +|The MomentJS format to use for bucketing. For example, `"YYYY-MM"` rounds to months. See https://momentjs.com/docs/#/displaying/. |=== *Returns:* `number` @@ -2322,44 +2196,42 @@ Creates an object used for describing the properties of a series on a chart. Use |=== |Argument |Type |Description -|`label` -|`string` -|The name of the series to style. +|`bars` +|`number` +|The width of bars. |`color` |`string` |The line color. +|`fill` +|`number`, `boolean` +|Should we fill in the points? + +Default: `false` + +|`horizontalBars` +|`boolean` +|Sets the orientation of the bars in the chart to horizontal. + +|`label` +|`string` +|The name of the series to style. + |`lines` |`number` |The width of the line. -|`bars` -|`number` -|The width of bars. - |`points` |`number` |The size of points on line. -|`fill` -|`number`, `boolean` -|Should we fill in the points? - -Default: `false` - |`stack` |`number`, `null` |Specifies if the series should be stacked. The number is the stack ID. Series with the same stack ID are stacked together. - -|`horizontalBars` -|`boolean` -|Sets the orientation of the bars in the chart to horizontal. - -Default: `false` |=== -*Returns:* `seriesStyle` +*Returns:* `seriesStyle` [float] @@ -2374,25 +2246,25 @@ Creates a shape. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `shape` |`string` -|Pick a shape +|Pick a shape. -Default: `"square"` +Default: `square` -|`border` +|`border` -Alias `stroke` -|`number` +Alias: `stroke` +|`string` |An SVG color for the border outlining the shape. -|`borderWidth` +|`borderWidth` Alias: `strokeWidth` |`number` -|The thickness of the border +|The thickness of the border. Default: `0` @@ -2409,7 +2281,7 @@ Default: `"black"` Default: `false` |=== -*Returns:* shape +*Returns:* `shape` [float] @@ -2426,13 +2298,13 @@ Sorts a `datatable` by the specified column. |_Unnamed_ -Alias: `column` +Aliases: `by`, `column` |`string` |The column to sort by. When unspecified, the `datatable` is sorted by the first column. |`reverse` |`boolean` -|Reverse the sorting order? When unspecified, the `datatable` is sorted in ascending order. +|Reverses the sorting order. When unspecified, the `datatable` is sorted in ascending order. Default: `false` |=== @@ -2452,15 +2324,15 @@ Adds a column with the same static value in every row. See also <>. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ Alias: `count` |`number` -|The number of rows to retrieve from the end of the datatable. +|The number of rows to retrieve from the end of the `datatable`. + +Default: `1` |=== *Returns:* `datatable` @@ -2596,29 +2469,29 @@ Creates a time filter for querying a source. |=== |Argument |Type |Description -|`column` +|`column` -Alias: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. Default: `"@timestamp"` -|`from` - -Alias: `f`, `start` +|`filterGroup` |`string` -|The beginning of the range, in ISO8601 or {es} `datemath` format +|The group name for the filter -|`to` +|`from` -Alias: `t`, `end` +Aliases: `f`, `start` |`string` -|The end of the range, in ISO8601 or {es} `datemath` format +|The beginning of the range, in ISO8601 or Elasticsearch `datemath` format -|`filterGroup` +|`to` + +Aliases: `end`, `t` |`string` -|The group name for the filter +|The end of the range, in ISO8601 or Elasticsearch `datemath` format |=== *Returns:* `filter` @@ -2636,13 +2509,13 @@ Configures a time filter control element. |=== |Argument |Type |Description -|`column` +|`column` -Alias: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. -Default: `"@timestamp"` +Default: `@timestamp` |`compact` |`boolean` @@ -2655,7 +2528,6 @@ Default: `true` |The group name for the filter. |=== - *Returns:* `render` @@ -2671,37 +2543,37 @@ Uses Timelion to extract one or more time series from many sources. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `q`, `query` |`string` -|A Timelion query +|A Timelion query Default: `".es(*)"` -|`interval` -|`string` -|The bucket interval for the time series - -Default: `"auto"` - |`from` |`string` -|The {es} `datemath` string for the beginning of the time range. +|The Elasticsearch `datemath` string for the beginning of the time range. Default: `"now-1y"` -|`to` +|`interval` |`string` -|The {es} `datemath` string for the end of the time range. +|The bucket interval for the time series. -Default: `"now"` +Default: `"auto"` |`timezone` |`string` -|The timezone for the time range. See [Moment Timezone](https://momentjs.com/timezone/). +|The timezone for the time range. See https://momentjs.com/timezone/. Default: `"UTC"` + +|`to` +|`string` +|The Elasticsearch `datemath` string for the end of the time range. + +Default: `"now"` |=== *Returns:* `datatable` @@ -2719,16 +2591,15 @@ Explicitly casts the type of the _context_ from one type to the specified type. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ † Alias: `type` |`string` -|A known type +|A known data type in the expression language. |=== *Returns:* Depends on your input and arguments - [float] [[u_fns]] == U @@ -2737,7 +2608,7 @@ Alias: `type` [[urlparam_fn]] === `urlparam` -Retrieves a URL parameter to use in an expression. The <> function always returns a `string`. For example, you can retrieve the value `"20"` from the parameter `myVar` from the URL `https://localhost:5601/app/canvas?myVar=20`. +Retrieves a URL parameter to use in an expression. The <> function always returns a `string`. For example, you can retrieve the value `"20"` from the parameter `myVar` from the URL `https://localhost:5601/app/canvas?myVar=20`. *Accepts:* `null` @@ -2745,9 +2616,9 @@ Retrieves a URL parameter to use in an expression. The <> function |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Aliases: `var`, `variable` +Aliases: `param`, `var`, `variable` |`string` |The URL hash parameter to retrieve. @@ -2759,4 +2630,3 @@ Default: `""` |=== *Returns:* `string` - diff --git a/docs/developer/add-data-guide.asciidoc b/docs/developer/add-data-guide.asciidoc index 9c44ae9c9ded3..aec44a9537ee8 100644 --- a/docs/developer/add-data-guide.asciidoc +++ b/docs/developer/add-data-guide.asciidoc @@ -11,10 +11,13 @@ Each tutorial contains three sets of instructions: [float] === Creating a new tutorial +// TODO: update path to where the directory must be created on the new platform 1. Create a new directory in the link:https://github.com/elastic/kibana/tree/master/src/legacy/core_plugins/kibana/server/tutorials[tutorials directory]. 2. In the new directory, create a file called `index.js` that exports a function. -The function must return a JavaScript object that conforms to the link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/common/tutorials/tutorial_schema.js[tutorial schema]. -3. Register the tutorial in link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/server/tutorials/register.js[register.js] by calling `server.registerTutorial(myFuncImportedFromIndexJs)`. +The function must return a JavaScript object that conforms to the link:https://github.com/elastic/kibana/blob/master/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts[tutorial schema]. +// TODO: update path to where the tutorial must be registered on the new platform +3. Register the tutorial in link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/server/tutorials/register.js[register.js] by calling `server.newPlatform.setup.plugins.home.tutorials.registerTutorial(myFuncImportedFromIndexJs)`. +// TODO: update path to where the image assets must be added on the new platform 4. Add image assets to the link:https://github.com/elastic/kibana/tree/master/src/legacy/core_plugins/kibana/public/home/tutorial_resources[tutorial_resources directory]. 5. Run Kibana locally to preview the tutorial. 6. Create a PR and go through the review process to get the changes approved. diff --git a/docs/developer/plugin/development-uiexports.asciidoc b/docs/developer/plugin/development-uiexports.asciidoc index de713416ae2cd..6368446f7fb43 100644 --- a/docs/developer/plugin/development-uiexports.asciidoc +++ b/docs/developer/plugin/development-uiexports.asciidoc @@ -8,7 +8,6 @@ An aggregate list of available UiExport types: | Type | Purpose | hacks | Any module that should be included in every application | visTypes | Modules that register providers with the `ui/registry/vis_types` registry. -| fieldFormats | Modules that register providers with the `ui/registry/field_formats` registry. | inspectorViews | Modules that register custom inspector views via the `viewRegistry` in `ui/inspector`. | chromeNavControls | Modules that register providers with the `ui/registry/chrome_nav_controls` registry. | navbarExtensions | Modules that register providers with the `ui/registry/navbar_extensions` registry. diff --git a/docs/developer/security/rbac.asciidoc b/docs/developer/security/rbac.asciidoc index b967dabf0684f..02b8233a9a3df 100644 --- a/docs/developer/security/rbac.asciidoc +++ b/docs/developer/security/rbac.asciidoc @@ -1,7 +1,14 @@ [[development-security-rbac]] === Role-based access control -Role-based access control (RBAC) in {kib} relies upon the {xpack-ref}/security-privileges.html#application-privileges[application privileges] that Elasticsearch exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. +Role-based access control (RBAC) in {kib} relies upon the +{ref}/security-privileges.html#application-privileges[application privileges] +that Elasticsearch exposes. This allows {kib} to define the privileges that +{kib} wishes to grant to users, assign them to the relevant users using roles, +and then authorize the user to perform a specific action. This is handled within +a secured instance of the `SavedObjectsClient` and available transparently to +consumers when using `request.getSavedObjectsClient()` or +`savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] ==== {kib} Privileges diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextension.appname.md b/docs/development/core/public/kibana-plugin-public.chromehelpextension.appname.md new file mode 100644 index 0000000000000..d817238c9287d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextension.appname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) > [appName](./kibana-plugin-public.chromehelpextension.appname.md) + +## ChromeHelpExtension.appName property + +Provide your plugin's name to create a header for separation + +Signature: + +```typescript +appName: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextension.content.md b/docs/development/core/public/kibana-plugin-public.chromehelpextension.content.md new file mode 100644 index 0000000000000..b51d4928e991d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextension.content.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) > [content](./kibana-plugin-public.chromehelpextension.content.md) + +## ChromeHelpExtension.content property + +Custom content to occur below the list of links + +Signature: + +```typescript +content?: (element: HTMLDivElement) => () => void; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextension.links.md b/docs/development/core/public/kibana-plugin-public.chromehelpextension.links.md new file mode 100644 index 0000000000000..de17ca8d86e37 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextension.links.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) > [links](./kibana-plugin-public.chromehelpextension.links.md) + +## ChromeHelpExtension.links property + +Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button + +Signature: + +```typescript +links?: ChromeHelpExtensionMenuLink[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextension.md b/docs/development/core/public/kibana-plugin-public.chromehelpextension.md index 82e7ba9bec4a3..6f0007335c555 100644 --- a/docs/development/core/public/kibana-plugin-public.chromehelpextension.md +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextension.md @@ -2,11 +2,20 @@ [Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) -## ChromeHelpExtension type +## ChromeHelpExtension interface Signature: ```typescript -export declare type ChromeHelpExtension = (element: HTMLDivElement) => () => void; +export interface ChromeHelpExtension ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appName](./kibana-plugin-public.chromehelpextension.appname.md) | string | Provide your plugin's name to create a header for separation | +| [content](./kibana-plugin-public.chromehelpextension.content.md) | (element: HTMLDivElement) => () => void | Custom content to occur below the list of links | +| [links](./kibana-plugin-public.chromehelpextension.links.md) | ChromeHelpExtensionMenuLink[] | Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button | + diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenucustomlink.md b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenucustomlink.md new file mode 100644 index 0000000000000..daca70f3b79c1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenucustomlink.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) + +## ChromeHelpExtensionMenuCustomLink type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { + linkType: 'custom'; + content: React.ReactNode; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudiscusslink.md b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudiscusslink.md new file mode 100644 index 0000000000000..8dd1c796bebf1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudiscusslink.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) + +## ChromeHelpExtensionMenuDiscussLink type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { + linkType: 'discuss'; + href: string; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudocumentationlink.md b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudocumentationlink.md new file mode 100644 index 0000000000000..0114cc245a874 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenudocumentationlink.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) + +## ChromeHelpExtensionMenuDocumentationLink type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { + linkType: 'documentation'; + href: string; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenugithublink.md b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenugithublink.md new file mode 100644 index 0000000000000..5dd33f1a05a7f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenugithublink.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) + +## ChromeHelpExtensionMenuGitHubLink type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { + linkType: 'github'; + labels: string[]; + title?: string; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenulink.md b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenulink.md new file mode 100644 index 0000000000000..072ce165e23c5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromehelpextensionmenulink.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) + +## ChromeHelpExtensionMenuLink type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index cec307032094e..22794ca945540 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -33,6 +33,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeDocTitle](./kibana-plugin-public.chromedoctitle.md) | APIs for accessing and updating the document title. | +| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) | | | [ChromeNavControls](./kibana-plugin-public.chromenavcontrols.md) | [APIs](./kibana-plugin-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | | [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | @@ -98,7 +99,11 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | -| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | +| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | | +| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-public.chromehelpextensionmenudiscusslink.md) | | +| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-public.chromehelpextensionmenudocumentationlink.md) | | +| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | | +| [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md new file mode 100644 index 0000000000000..28141c9e13749 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) > [config](./kibana-plugin-public.plugininitializercontext.config.md) + +## PluginInitializerContext.config property + +Signature: + +```typescript +readonly config: { + get: () => T; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index 87c39a502040d..64eaabb28646d 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -9,13 +9,14 @@ The available core services passed to a `PluginInitializer` Signature: ```typescript -export interface PluginInitializerContext +export interface PluginInitializerContext ``` ## Properties | Property | Type | Description | | --- | --- | --- | +| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
get: <T extends object = ConfigSchema>() => T;
} | | | [env](./kibana-plugin-public.plugininitializercontext.env.md) | {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
} | | | [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 866755e78648a..cecceb04240e6 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 50451b813a61c..c4ceb47f66e1b 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 9124b4a1b21c4..5202e0cfd5ebb 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete:

(route: RouteConfig, handler: RequestHandler) => void; +delete: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 0291906c6fc6b..32552a49cb999 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get:

(route: RouteConfig, handler: RequestHandler) => void; +get: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md new file mode 100644 index 0000000000000..2367420068064 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) + +## IRouter.handleLegacyErrors property + +Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + +Signature: + +```typescript +handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index bbffe1e42f229..b5d3c893d745d 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,9 +16,10 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | -| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar | Register a route handler for GET request. | +| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index e97a32e433ce9..cd655c9ce0dc8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post:

(route: RouteConfig, handler: RequestHandler) => void; +post: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index 25db91e389939..e553d4b79dd2b 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put:

(route: RouteConfig, handler: RequestHandler) => void; +put: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9907750b8742f..360675b3490c2 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -75,6 +75,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | @@ -156,6 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | +| [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | | @@ -168,6 +170,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | +| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md new file mode 100644 index 0000000000000..d62b2457e9d9a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) + +## PluginConfigDescriptor.exposeToBrowser property + +List of configuration properties that will be available on the client-side plugin. + +Signature: + +```typescript +exposeToBrowser?: { + [P in keyof T]?: boolean; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md new file mode 100644 index 0000000000000..41fdcfe5df45d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) + +## PluginConfigDescriptor interface + +Describes a plugin configuration schema and capabilities. + +Signature: + +```typescript +export interface PluginConfigDescriptor +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | +| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | + +## Example + + +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + uiProp: true, + }, + schema: configSchema, +}; + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md new file mode 100644 index 0000000000000..c4845d52ff212 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) + +## PluginConfigDescriptor.schema property + +Schema to use to validate the plugin configuration. + +[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) + +Signature: + +```typescript +schema: PluginConfigSchema; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md new file mode 100644 index 0000000000000..6528798ec8e01 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) + +## PluginConfigSchema type + +Dedicated type for plugin configuration schema. + +Signature: + +```typescript +export declare type PluginConfigSchema = Type; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 2b3ff9a2cd419..248726e26f393 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,5 +16,5 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
internal: Map<PluginName, InternalPluginInfo>;
public: Map<PluginName, DiscoveredPlugin>;
browserConfigs: Map<PluginName, Observable<unknown>>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index fa286dfb59092..7c47304cb9bf6 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -8,7 +8,8 @@ ```typescript uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md new file mode 100644 index 0000000000000..535927dc73743 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) + +## RouteRegistrar type + +Handler to declare a route. + +Signature: + +```typescript +export declare type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/discover/context.asciidoc b/docs/discover/context.asciidoc index 2c85358f84d52..9049109d6124d 100644 --- a/docs/discover/context.asciidoc +++ b/docs/discover/context.asciidoc @@ -3,7 +3,7 @@ For certain applications it can be useful to inspect a window of documents surrounding a specific event. The context view enables you to do just that for -index patterns that are configured to contain time-based events. +<> that are configured to contain time-based events. To show the context surrounding an anchor document, click the *Expand* button image:images/ExpandButton.jpg[Expand Button] to the left of the document's diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc index a85489a947cea..dc6a45dc5ad7e 100644 --- a/docs/discover/document-data.asciidoc +++ b/docs/discover/document-data.asciidoc @@ -5,7 +5,7 @@ When you submit a search query, the 500 most recent documents that match the que are listed in the Documents table. You can configure the number of documents shown in the table by setting the `discover:sampleSize` property in <>. By default, the table shows the localized version of the time -field configured for the selected index pattern and the document `_source`. You can +field configured for the selected <> and the document `_source`. You can <> from the Fields list. You can <> by any indexed field that's included in the table. diff --git a/docs/discover/field-filter.asciidoc b/docs/discover/field-filter.asciidoc index 98a3b90617a71..5646fe079401e 100644 --- a/docs/discover/field-filter.asciidoc +++ b/docs/discover/field-filter.asciidoc @@ -14,7 +14,8 @@ To add a filter from the Fields list: . Click the name of the field you want to filter on. This displays the top five values for that field. + -image::images/filter-field.jpg[] +[role="screenshot"] +image::images/filter-field.png[height=317] . To add a positive filter, click the *Positive Filter* button image:images/PositiveFilter.jpg[Positive Filter]. This includes only those documents that contain that value in the field. @@ -43,8 +44,7 @@ field name. This includes only those documents that contain the field. To manually add a filter: . Click *Add Filter*. A popup will be displayed for you to create the filter. -+ -image::images/add_filter.png[] + . Choose a field to filter by. This list of fields will include fields from the index pattern you are currently querying against. + @@ -78,26 +78,26 @@ turn off the suggestions by setting the advanced setting, `filterEditor:suggestV [[filter-pinning]] === Managing Filters -To modify a filter, hover over it and click one of the action buttons. +To modify a filter, click on it and click one of the action buttons. image::images/filter-allbuttons.png[]   -image:images/filter-enable.png[] Enable Filter :: Disable the filter without -removing it. Click again to reenable the filter. Diagonal stripes indicate -that a filter is disabled. -image:images/filter-pin.png[] Pin Filter :: Pin the filter. Pinned filters +Pin across all apps :: Pinned filters persist when you switch contexts in Kibana. For example, you can pin a filter in Discover and it remains in place when you switch to Visualize. Note that a filter is based on a particular index field--if the indices being searched don't contain the field in a pinned filter, it has no effect. -image:images/filter-toggle.png[] Invert Filter :: Switch from a positive -filter to a negative filter and vice-versa. -image:images/filter-delete.png[] Remove Filter :: Remove the filter. -image:images/filter-custom.png[] Edit Filter :: <> definition. Enables you to manually update the filter and specify a label for the filter. +Exclude results :: Switch from a positive +filter to a negative filter and vice-versa. +Temporarily disable :: Disable the filter without +removing it. Click again to reenable the filter. Diagonal stripes indicate +that a filter is disabled. +Remove Filter :: Remove the filter. To apply a filter action to all of the applied filters, click *Actions* and select the action. diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index cfca4f2fc092b..9c4e406455c27 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -1,7 +1,7 @@ [[search]] == Searching your data -You can search the indices that match the current index pattern by entering -your search criteria in the Query bar. By default you can use Kibana's standard query language +You can search the indices that match the current <> by entering +your search criteria in the Query bar. By default you can use Kibana's <> which features autocomplete and a simple, easy to use syntax. Kibana's legacy query language (based on Lucene https://lucene.apache.org/core/2_9_4/queryparsersyntax.html[query syntax]) is still available for the time being under the options menu in the Query Bar. When this diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc index c2d366cdcbbb6..c53850b38a2b0 100644 --- a/docs/discover/set-time-filter.asciidoc +++ b/docs/discover/set-time-filter.asciidoc @@ -1,7 +1,7 @@ [[set-time-filter]] == Setting the time filter If your index contains time-based events, and a time-field is configured for the -selected index pattern, set a time filter that displays only the data within the +selected <>, set a time filter that displays only the data within the specified time range. You can use the time filter to change the time range, or select a specific time diff --git a/docs/discover/viewing-field-stats.asciidoc b/docs/discover/viewing-field-stats.asciidoc index d9fd3b9eb033b..96a26c78596e2 100644 --- a/docs/discover/viewing-field-stats.asciidoc +++ b/docs/discover/viewing-field-stats.asciidoc @@ -11,4 +11,4 @@ they are available in the side bar if we uncheck "Hide missing fields". To view field data statistics, click the name of a field in the Fields list. -image:images/filter-field.jpg[Field Statistics] \ No newline at end of file +image:images/filter-field.png[Field Statistics,height=317] \ No newline at end of file diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index eafbb7d8f7c91..a05205fceab4a 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -91,7 +91,7 @@ and whether it's _tokenized_, or broken up into separate words. NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] +index privileges. See {ref}/security-privileges.html[Security privileges] for more information. In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 24cc176d5daf9..f41c648a3d492 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -12,8 +12,8 @@ with Kibana sample data and learn to: NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. +on the `kibana_sample_data_*` indices. See +{ref}/security-privileges.html[Security privileges] for more information. [float] diff --git a/docs/images/add-bucket.png b/docs/images/add-bucket.png new file mode 100644 index 0000000000000..acfba7366363e Binary files /dev/null and b/docs/images/add-bucket.png differ diff --git a/docs/images/add_filter.png b/docs/images/add_filter.png deleted file mode 100644 index 0591472c5c9ea..0000000000000 Binary files a/docs/images/add_filter.png and /dev/null differ diff --git a/docs/images/add_filter_field.png b/docs/images/add_filter_field.png index f2093ab94e727..2052559cf5273 100644 Binary files a/docs/images/add_filter_field.png and b/docs/images/add_filter_field.png differ diff --git a/docs/images/add_filter_operator.png b/docs/images/add_filter_operator.png index dc2355e8cb2b1..fd7d42a9d1b98 100644 Binary files a/docs/images/add_filter_operator.png and b/docs/images/add_filter_operator.png differ diff --git a/docs/images/add_filter_value.png b/docs/images/add_filter_value.png index 15eeab73943c6..d357c6e5a3013 100644 Binary files a/docs/images/add_filter_value.png and b/docs/images/add_filter_value.png differ diff --git a/docs/images/bar-terms-agg.jpg b/docs/images/bar-terms-agg.jpg deleted file mode 100644 index dc815cc0030b9..0000000000000 Binary files a/docs/images/bar-terms-agg.jpg and /dev/null differ diff --git a/docs/images/bar-terms-agg.png b/docs/images/bar-terms-agg.png new file mode 100644 index 0000000000000..b0b62b9e53213 Binary files /dev/null and b/docs/images/bar-terms-agg.png differ diff --git a/docs/images/bar-terms-subagg.jpg b/docs/images/bar-terms-subagg.jpg deleted file mode 100644 index 7c8e5e5c0be31..0000000000000 Binary files a/docs/images/bar-terms-subagg.jpg and /dev/null differ diff --git a/docs/images/bar-terms-subagg.png b/docs/images/bar-terms-subagg.png new file mode 100644 index 0000000000000..37cf5486eff1e Binary files /dev/null and b/docs/images/bar-terms-subagg.png differ diff --git a/docs/images/canvas-add-pages.gif b/docs/images/canvas-add-pages.gif index 99d3c3d6229a4..5f977c6afb10f 100644 Binary files a/docs/images/canvas-add-pages.gif and b/docs/images/canvas-add-pages.gif differ diff --git a/docs/images/canvas-align-elements.gif b/docs/images/canvas-align-elements.gif index 10530df4f83b9..d2e2cda475757 100644 Binary files a/docs/images/canvas-align-elements.gif and b/docs/images/canvas-align-elements.gif differ diff --git a/docs/images/canvas-background-color-picker-min.gif b/docs/images/canvas-background-color-picker-min.gif new file mode 100644 index 0000000000000..bd22941b35f5d Binary files /dev/null and b/docs/images/canvas-background-color-picker-min.gif differ diff --git a/docs/images/canvas-create-URL-min.gif b/docs/images/canvas-create-URL-min.gif new file mode 100644 index 0000000000000..0c9fbf7201d80 Binary files /dev/null and b/docs/images/canvas-create-URL-min.gif differ diff --git a/docs/images/canvas-distribute-elements.gif b/docs/images/canvas-distribute-elements.gif index d4b6985ac7e52..2d7eca338d275 100644 Binary files a/docs/images/canvas-distribute-elements.gif and b/docs/images/canvas-distribute-elements.gif differ diff --git a/docs/images/canvas-element-select.gif b/docs/images/canvas-element-select.gif index db749f5e9397a..9ead1c3243ee1 100644 Binary files a/docs/images/canvas-element-select.gif and b/docs/images/canvas-element-select.gif differ diff --git a/docs/images/canvas-embed_workpad-min.gif b/docs/images/canvas-embed_workpad-min.gif new file mode 100644 index 0000000000000..293792c7fbcf2 Binary files /dev/null and b/docs/images/canvas-embed_workpad-min.gif differ diff --git a/docs/images/canvas-fullscreen.gif b/docs/images/canvas-fullscreen.gif index 5a06f9158ecf6..2eebd3b511000 100644 Binary files a/docs/images/canvas-fullscreen.gif and b/docs/images/canvas-fullscreen.gif differ diff --git a/docs/images/canvas-fullscreen.png b/docs/images/canvas-fullscreen.png index b26e9493f17b5..7e6ec6ad7e7a8 100644 Binary files a/docs/images/canvas-fullscreen.png and b/docs/images/canvas-fullscreen.png differ diff --git a/docs/images/canvas-generate-pdf-min.gif b/docs/images/canvas-generate-pdf-min.gif new file mode 100644 index 0000000000000..9ef16dc1e5017 Binary files /dev/null and b/docs/images/canvas-generate-pdf-min.gif differ diff --git a/docs/images/canvas-zoom-controls.png b/docs/images/canvas-zoom-controls.png index e0fc020e262a7..a7d2820a58925 100644 Binary files a/docs/images/canvas-zoom-controls.png and b/docs/images/canvas-zoom-controls.png differ diff --git a/docs/images/canvas_share_autoplay_480-min.gif b/docs/images/canvas_share_autoplay_480-min.gif new file mode 100644 index 0000000000000..84a108e58d3dc Binary files /dev/null and b/docs/images/canvas_share_autoplay_480-min.gif differ diff --git a/docs/images/canvas_share_hidetoolbar_480-min.gif b/docs/images/canvas_share_hidetoolbar_480-min.gif new file mode 100644 index 0000000000000..4706db48967b5 Binary files /dev/null and b/docs/images/canvas_share_hidetoolbar_480-min.gif differ diff --git a/docs/images/color-picker.png b/docs/images/color-picker.png index a1148d3f4b1df..ebfa49b5c0442 100644 Binary files a/docs/images/color-picker.png and b/docs/images/color-picker.png differ diff --git a/docs/images/edit_filter_query.png b/docs/images/edit_filter_query.png index 5a0612f17eaf9..367a2a8578b8b 100644 Binary files a/docs/images/edit_filter_query.png and b/docs/images/edit_filter_query.png differ diff --git a/docs/images/edit_filter_query_json.png b/docs/images/edit_filter_query_json.png index 242f4610e097f..0dfc3e8df8763 100644 Binary files a/docs/images/edit_filter_query_json.png and b/docs/images/edit_filter_query_json.png differ diff --git a/docs/images/filter-allbuttons.png b/docs/images/filter-allbuttons.png index 8bb86f53a5631..3d6951812daa7 100644 Binary files a/docs/images/filter-allbuttons.png and b/docs/images/filter-allbuttons.png differ diff --git a/docs/images/filter-custom.png b/docs/images/filter-custom.png deleted file mode 100644 index d871b4c52970b..0000000000000 Binary files a/docs/images/filter-custom.png and /dev/null differ diff --git a/docs/images/filter-delete.png b/docs/images/filter-delete.png deleted file mode 100644 index 13845303c491d..0000000000000 Binary files a/docs/images/filter-delete.png and /dev/null differ diff --git a/docs/images/filter-enable.png b/docs/images/filter-enable.png deleted file mode 100644 index 48d9bb3e1cb49..0000000000000 Binary files a/docs/images/filter-enable.png and /dev/null differ diff --git a/docs/images/filter-field.jpg b/docs/images/filter-field.jpg deleted file mode 100644 index 9b30e30df4638..0000000000000 Binary files a/docs/images/filter-field.jpg and /dev/null differ diff --git a/docs/images/filter-field.png b/docs/images/filter-field.png new file mode 100644 index 0000000000000..dd6ee72df93c9 Binary files /dev/null and b/docs/images/filter-field.png differ diff --git a/docs/images/filter-pin.png b/docs/images/filter-pin.png deleted file mode 100644 index 4f7eef0a3ae42..0000000000000 Binary files a/docs/images/filter-pin.png and /dev/null differ diff --git a/docs/images/filter-toggle.png b/docs/images/filter-toggle.png deleted file mode 100644 index 7f47a681c05b5..0000000000000 Binary files a/docs/images/filter-toggle.png and /dev/null differ diff --git a/docs/images/gauge.png b/docs/images/gauge.png new file mode 100644 index 0000000000000..b20d99f55268b Binary files /dev/null and b/docs/images/gauge.png differ diff --git a/docs/images/goal.png b/docs/images/goal.png new file mode 100644 index 0000000000000..04f16e8cd3e74 Binary files /dev/null and b/docs/images/goal.png differ diff --git a/docs/images/lens_data_info-min.gif b/docs/images/lens_data_info-min.gif new file mode 100644 index 0000000000000..39f9f668df4cd Binary files /dev/null and b/docs/images/lens_data_info-min.gif differ diff --git a/docs/images/lens_drag_drop-min.gif b/docs/images/lens_drag_drop-min.gif new file mode 100644 index 0000000000000..7219742114190 Binary files /dev/null and b/docs/images/lens_drag_drop-min.gif differ diff --git a/docs/images/lens_suggestions-min.gif b/docs/images/lens_suggestions-min.gif new file mode 100644 index 0000000000000..de1d5b7096864 Binary files /dev/null and b/docs/images/lens_suggestions-min.gif differ diff --git a/docs/images/time_range_per_panel-min.gif b/docs/images/time_range_per_panel-min.gif new file mode 100644 index 0000000000000..04d2e2f17b4f4 Binary files /dev/null and b/docs/images/time_range_per_panel-min.gif differ diff --git a/docs/images/visualize-date-histogram-split-1.png b/docs/images/visualize-date-histogram-split-1.png new file mode 100644 index 0000000000000..3036d82a01759 Binary files /dev/null and b/docs/images/visualize-date-histogram-split-1.png differ diff --git a/docs/images/visualize-date-histogram-split-2.png b/docs/images/visualize-date-histogram-split-2.png new file mode 100644 index 0000000000000..4bc6e4b49c813 Binary files /dev/null and b/docs/images/visualize-date-histogram-split-2.png differ diff --git a/docs/images/visualize-date-histogram.png b/docs/images/visualize-date-histogram.png new file mode 100644 index 0000000000000..4380ea9703f12 Binary files /dev/null and b/docs/images/visualize-date-histogram.png differ diff --git a/docs/images/visualize-drag-reorder.png b/docs/images/visualize-drag-reorder.png new file mode 100644 index 0000000000000..a886a19c69f88 Binary files /dev/null and b/docs/images/visualize-drag-reorder.png differ diff --git a/docs/logs/images/analysis-tab-create-ml-job.png b/docs/logs/images/analysis-tab-create-ml-job.png new file mode 100644 index 0000000000000..0f4115bb93f4c Binary files /dev/null and b/docs/logs/images/analysis-tab-create-ml-job.png differ diff --git a/docs/logs/images/log-rate-anomalies.png b/docs/logs/images/log-rate-anomalies.png new file mode 100644 index 0000000000000..ac9ff7c9a5235 Binary files /dev/null and b/docs/logs/images/log-rate-anomalies.png differ diff --git a/docs/logs/images/log-rate-entries.png b/docs/logs/images/log-rate-entries.png new file mode 100644 index 0000000000000..f8a3acc9883e0 Binary files /dev/null and b/docs/logs/images/log-rate-entries.png differ diff --git a/docs/logs/images/log-time-filter.png b/docs/logs/images/log-time-filter.png new file mode 100644 index 0000000000000..863e488e6c6c0 Binary files /dev/null and b/docs/logs/images/log-time-filter.png differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc index d16bfb482aca4..edbadde223103 100644 --- a/docs/logs/index.asciidoc +++ b/docs/logs/index.asciidoc @@ -27,3 +27,5 @@ include::getting-started.asciidoc[] include::using.asciidoc[] include::configuring.asciidoc[] + +include::log-rate.asciidoc[] diff --git a/docs/logs/log-rate.asciidoc b/docs/logs/log-rate.asciidoc new file mode 100644 index 0000000000000..56284a1c76219 --- /dev/null +++ b/docs/logs/log-rate.asciidoc @@ -0,0 +1,94 @@ +[role="xpack"] +[[xpack-logs-analysis]] +== Detecting and inspecting log anomalies + +beta::[] + +When the {ml} {anomaly-detect} features are enabled, +you can use the **Log rate** page in the Logs app. +**Log rate** helps you to detect and inspect log anomalies and the log partitions where the log anomalies occur. +This means you can easily spot anomalous behavior without significant human intervention -- +no more manually sampling log data, calculating rates, and determining if rates are normal. + +*Log rate* automatically highlights periods of time where the log rate is outside expected bounds, +and therefore may be anomalous. +You can use this information as a basis for further investigations. +For example: + +* A significant drop in the log rate might suggest that a piece of infrastructure stopped responding, +and thus we're serving less requests. +* A spike in the log rate could denote a DDoS attack. +This may lead to an investigation of IP addresses from incoming requests. + +You can also view log anomalies directly in the <>. + +[float] +[[logs-analysis-create-ml-job]] +=== Enable log rate analysis and anomaly detection + +Create a machine learning job to enable log rate analysis and anomaly detection. + +[role="screenshot"] +image::logs/images/analysis-tab-create-ml-job.png[Create machine learning job] + +1. To enable log rate analysis and anomaly detection, +you must first create your own {kibana-ref}/xpack-spaces.html[space]. +2. Within a space, navigate to the Logs app and select *Log rate*. +Here, you'll be prompted to create a machine learning job which will carry out the log rate analysis. +3. Choose a time range for the machine learning analysis. +4. Add the Indices that contain the logs you want to analyze. +5. Click *Create ML job*. +6. You're now ready to analyze your log partitions. + +Even though the machine learning job's time range is fixed, +you can still use the time filter to adjust the results that are shown in your analysis. + +[role="screenshot"] +image::logs/images/log-time-filter.png[Log rate time filter] + +[float] +[[logs-analysis-entries-chart]] +=== Log entries chart + +The log entries chart shows an overall, color-coded visualization of the log entry rate, +partitioned according to the value of the Elastic Common Schema (ECS) +{ecs-ref}/ecs-event.html[`event.dataset`] field. +This chart helps you quickly spot increases or decreases in each partition's log rate. + +[role="screenshot"] +image::logs/images/log-rate-entries.png[Log rate entries chart] + +If you have a lot of log partitions, use the following to filter your data: + +* Hover over a time range to see the log rate for each partition. +* Click or hover on a partition name to show, hide, or highlight the partition values. + +[float] +[[logs-analysis-anomalies-chart]] +=== Anomalies charts + +The Anomalies chart shows the time range where anomalies were detected. +The typical rate values are shown in grey, while the anomalous regions are color-coded and superimposed on top. + +[role="screenshot"] +image::logs/images/log-rate-anomalies.png[Log rate entries chart] + +When a time range is flagged as anomalous, +the machine learning algorithms have detected unusual log rate activity. +This might be because: + +* The log rate is significantly higher than usual. +* The log rate is significantly lower than usual. +* Other anomalous behavior has been detected. +For example, the log rate is within bounds, but not fluctuating when it is expected to. + +The level of anomaly detected in a time period is color-coded, from red, orange, yellow, to blue. +Red indicates a critical anomaly level, while blue is a warning level. + +To help you further drill down into a potential anomaly, +you can view an anomaly chart for each individual partition: + +Anomaly scores range from 0 (no anomalies) to 100 (critical). + +To analyze the anomalies in more detail, click *Analyze in ML*, which opens the +{kibana-ref}/xpack-ml.html[Anomaly Explorer in Machine Learning]. diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index 916ad42a6d221..f191f7d746cf8 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -78,8 +78,19 @@ This opens the *Log event document details* fly-out that shows the fields associ To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. This automatically adds a search filter to the logs stream to filter the entries by this field and value. -To see other actions related to the event, in the log event details, click *Actions*. -Depending on the event and the features you have installed and configured, you may also be able to: +[float] +[[view-log-anomalies]] +=== View log anomalies + +When the machine learning anomaly detection features are enabled, click *Log rate*, which allows you to +<> in your log data. + +[float] +[[logs-integrations]] +=== Logs app integrations + +To see other actions related to the event, click *Actions* in the log event details. +Depending on the event and the features you have configured, you may also be able to: * Select *View status in Uptime* to <> in the *Uptime* app. * Select *View in APM* to <> in the *APM* app. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 38fceeb47d6fd..977a65f62202d 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -46,6 +46,7 @@ adapt to the interval between measurements. Keys are http://en.wikipedia.org/wik `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. `dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. `defaultIndex`:: The index to access if no index is set. The default is `null`. +`defaultRoute`:: The default route when opening Kibana. Use this setting to route users to a specific dashboard, application, or saved object as they enter each space. `fields:popularLimit`:: The top N most popular fields to show. `filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. `filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state (be pinned) by default. diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 6bfd36bc1c067..308e61abf70e5 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -29,7 +29,7 @@ include::field-formatters/url-formatter.asciidoc[] Date fields support the `Date`, `Url`, and `String` formatters. -The `Date` formatter enables you to choose the display format of date stamps using the http://moment.js[moment.js] +The `Date` formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] standard format definitions. include::field-formatters/string-formatter.asciidoc[] @@ -65,7 +65,7 @@ the https://adamwdraper.github.io/Numeral-js/[numeral.js] standard format defini Scripted fields compute data on the fly from the data in your Elasticsearch indices. Scripted field data is shown on the Discover tab as part of the document data, and you can use scripted fields in your visualizations. Scripted field values are computed at query time so they aren't indexed and cannot be searched using Kibana's default -query language. However they can be queried using Kibana's new <>. Scripted +query language. However they can be queried using Kibana's new <>. Scripted fields are also supported in the filter bar. WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 4a736e3ddab59..4c7f6c2aee6e6 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -22,7 +22,7 @@ If security is enabled, you must have the `monitor` cluster privilege and the `view_index_metadata` and `manage` index privileges to view the data. For index templates, you must have the `manage_index_templates` cluster privilege. -See {xpack-ref}/security-privileges.html[Security Privileges] for more +See {ref}/security-privileges.html[Security privileges] for more information. Before using this feature, you should be familiar with index management diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index 58885ae04605d..cc27eca4c267e 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -15,6 +15,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy * https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation * https://github.com/samtecspg/conveyor[Conveyor] - Simple (GUI) interface for importing data into Elasticsearch. +* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. * https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API * https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 920c448acf6db..65de5c4f93d12 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -45,3 +45,15 @@ This page was deleted. See <> and <>. Using the `kibana_dashboard_only_user` role is deprecated. Use <> instead. + +[role="exclude",id="pdf-layout-modes"] +== PDF layout modes + +This page has moved. Please see <>. + +[role="exclude",id="xpack-reporting"] +== Reporting from Kibana + +This page has moved. Please see <>. + + diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 68dd9a8b3cefb..2fc74d2ffee32 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -45,14 +45,26 @@ production cluster as well as monitor data sent to a dedicated monitoring cluster. `xpack.monitoring.elasticsearch.username`:: -Specifies the user ID that {kib} uses for authentication when it retrieves data -from the monitoring cluster. If not set, {kib} uses the value of the -`elasticsearch.username` setting. +Specifies the username used by {kib} monitoring to establish a persistent connection +in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} +monitoring cluster. + +Every other request performed by the Stack Monitoring UI to the monitoring {es} +cluster uses the authenticated user's credentials, which must be the same on +both the {es} monitoring cluster and the {es} production cluster. + +If not set, {kib} uses the value of the `elasticsearch.username` setting. `xpack.monitoring.elasticsearch.password`:: -Specifies the password that {kib} uses for authentication when it retrieves data -from the monitoring cluster. If not set, {kib} uses the value of the -`elasticsearch.password` setting. +Specifies the password used by {kib} monitoring to establish a persistent connection +in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} +monitoring cluster. + +Every other request performed by the Stack Monitoring UI to the monitoring {es} +cluster uses the authenticated user's credentials, which must be the same on +both the {es} monitoring cluster and the {es} production cluster. + +If not set, {kib} uses the value of the `elasticsearch.password` setting. `telemetry.enabled`:: Set to `true` (default) to send cluster statistics to Elastic. Reporting your diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 70f07cfc4019c..a754f91e9f22a 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -26,9 +26,24 @@ setting to preserve the same key across multiple restarts and multiple instances [[reporting-kibana-server-settings]] ==== Kibana server settings -Reporting uses the Kibana interface to generate reports. In most cases, you don't need -to configure Reporting to communicate with Kibana. However, if you use a reverse-proxy -to access Kibana, you must set the proxy port, protocol, and hostname. +Reporting opens the {kib} web interface in a server process to generate +screenshots of {kib} visualizations. In most cases, the default settings +will work and you don't need to configure Reporting to communicate with {kib}. +However, if your client connections must go through a reverse-proxy +to access {kib}, Reporting configuration must have the proxy port, protocol, +and hostname set in the `xpack.reporting.kibanaServer.*` settings. + +[NOTE] +==== +If a reverse-proxy carries encrypted traffic from end-user +clients back to a {kib} server, the proxy port, protocol, and hostname +in Reporting settings must be valid for the encryption that the Reporting +browser will receive. Encrypted communications will fail if there are +mismatches in the host information between the request and the certificate on the server. + +Configuring the `xpack.reporting.kibanaServer` settings to point to a +proxy host requires that the Kibana server has network access to the proxy. +==== `xpack.reporting.kibanaServer.port`:: The port for accessing Kibana, if different from the `server.port` value. @@ -39,8 +54,6 @@ The protocol for accessing Kibana, typically `http` or `https`. `xpack.reporting.kibanaServer.hostname`:: The hostname for accessing {kib}, if different from the `server.host` value. -NOTE: Configuring the `xpack.reporting.kibanaServer` settings to point to a -proxy host requires that the Kibana server has network access to the proxy. [float] [[reporting-job-queue-settings]] diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 2ba1369369a66..a2c05e4d87325 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -20,7 +20,7 @@ are enabled. Do not set this to `false`; it disables the login form, user and role management screens, and authorization using <>. To disable {security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. +{ref}/security-settings.html[{es} security settings]. `xpack.security.audit.enabled`:: Set to `true` to enable audit logging for security events. By default, it is set @@ -40,6 +40,8 @@ An arbitrary string of 32 characters or more that is used to encrypt credentials in a cookie. It is crucial that this key is not exposed to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. +In addition, high-availability deployments of {kib} will behave unexpectedly +if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: Sets the `secure` flag of the session cookie. The default value is `false`. It @@ -47,7 +49,16 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -`xpack.security.sessionTimeout`:: +`xpack.security.session.idleTimeout`:: Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit timeout, closing the -browser still requires the user to log back in to {kib}. +until the browser is closed. When this is set to an explicit idle timeout, closing +the browser still requires the user to log back in to {kib}. + +`xpack.security.session.lifespan`:: +Sets the maximum duration (in milliseconds), also known as "absolute timeout". By +default, a session can be renewed indefinitely. When this value is set, a session +will end once its lifespan is exceeded, even if the user is not idle. NOTE: if +`idleTimeout` is not set, this setting will still cause sessions to expire. + +`xpack.security.loginAssistanceMessage`:: +Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index b0893a6e78945..286fed34f64c5 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -54,8 +54,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{xpack-ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring Security in Kibana] for +{ref}/elasticsearch-security.html[{security}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/setup/install/brew.asciidoc b/docs/setup/install/brew.asciidoc index ad531a83d3690..3fe104bd04794 100644 --- a/docs/setup/install/brew.asciidoc +++ b/docs/setup/install/brew.asciidoc @@ -32,13 +32,13 @@ and data directory are stored in the following locations. | Type | Description | Default Location | Setting | home | Kibana home directory or `$KIBANA_HOME` - | /usr/local/var/homebrew/linked/kibana + | /usr/local/var/homebrew/linked/kibana-full d| | bin | Binary scripts including `kibana` to start a node and `kibana-plugin` to install plugins - | /usr/local/var/homebrew/linked/kibana/bin + | /usr/local/var/homebrew/linked/kibana-full/bin d| | conf @@ -59,7 +59,7 @@ and data directory are stored in the following locations. | plugins | Plugin files location. Each plugin will be contained in a subdirectory. - | /usr/local/var/homebrew/linked/kibana/plugins + | /usr/local/var/homebrew/linked/kibana-full/plugins d| |======================================================================= diff --git a/docs/setup/install/systemd.asciidoc b/docs/setup/install/systemd.asciidoc index 07d995244d511..3053972f9e384 100644 --- a/docs/setup/install/systemd.asciidoc +++ b/docs/setup/install/systemd.asciidoc @@ -18,5 +18,5 @@ sudo systemctl stop kibana.service -------------------------------------------- These commands provide no feedback as to whether Kibana was started -successfully or not. Instead, this information will be written in the log -files located in `/var/log/kibana/`. +successfully or not. Log information can be accessed via +`journalctl -u kibana.service`. diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index 138431aca22fa..e3104520292ff 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -32,13 +32,13 @@ The Linux archive for Kibana v{version} can be downloaded and installed as follo ["source","sh",subs="attributes"] -------------------------------------------- -wget https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz -shasum -a 512 kibana-{version}-linux-x86_64.tar.gz <1> +curl -O https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz +curl https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz.sha512 | shasum -a 512 -c - <1> tar -xzf kibana-{version}-linux-x86_64.tar.gz cd kibana-{version}-linux-x86_64/ <2> -------------------------------------------- -<1> Compare the SHA produced by `shasum` with the - https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz.sha512[published SHA]. +<1> Compares the SHA of the downloaded `.tar.gz` archive and the published checksum, which should output + `kibana-{version}-linux-x86_64.tar.gz: OK`. <2> This directory is known as `$KIBANA_HOME`. endif::[] @@ -60,12 +60,12 @@ The Darwin archive for Kibana v{version} can be downloaded and installed as foll ["source","sh",subs="attributes"] -------------------------------------------- curl -O https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz -shasum -a 512 kibana-{version}-darwin-x86_64.tar.gz <1> +curl https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz.sha512 | shasum -a 512 -c - <1> tar -xzf kibana-{version}-darwin-x86_64.tar.gz cd kibana-{version}-darwin-x86_64/ <2> -------------------------------------------- -<1> Compare the SHA produced by `shasum` with the - https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz.sha512[published SHA]. +<1> Compares the SHA of the downloaded `.tar.gz` archive and the published checksum, which should output + `kibana-{version}-darwin-x86_64.tar.gz: OK`. <2> This directory is known as `$KIBANA_HOME`. Alternatively, you can download the following package, which contains only diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f4434ea7a09f4..f2c06a3737c7c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -93,8 +93,8 @@ the configured certificate. `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL certificate and key files. These files are used to verify the identity of Kibana to Elasticsearch and are -required when `xpack.ssl.verification_mode` in Elasticsearch is set to either -`certificate` or `full`. +required when `xpack.security.http.ssl.client_authentication` in Elasticsearch is +set to `required`. `elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you to specify a list of paths to the PEM file for the certificate authority for diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index c947e000c8138..f56baf6abdc2e 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -24,7 +24,7 @@ Kibana provides step-by-step instructions to help you add data. The detailed information and instructions. [float] -=== {Beats} +=== {Beats} https://www.elastic.co/products/beats/auditbeat[{auditbeat}], https://www.elastic.co/products/beats/filebeat[{filebeat}], @@ -33,9 +33,14 @@ https://www.elastic.co/products/beats/packetbeat[{packetbeat}] send security events and other data to Elasticsearch. The default index patterns for SIEM events are `auditbeat-*`, `winlogbeat-*`, -`filebeat-*`, and `packetbeat-*``. You can change the default index patterns in +`filebeat-*`, `endgame-*`, and `packetbeat-*``. You can change the default index patterns in *Kibana > Management > Advanced Settings > siem:defaultIndex*. +[float] +=== Elastic Endpoint Sensor Management Platform + +The Elastic Endpoint Sensor Management Platform (SMP) ships host and network events directly to the SIEM application, and is fully ECS compliant. + [float] === Elastic Common Schema (ECS) for normalizing data diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index fc858eb6d86ef..69655aac521e7 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -22,6 +22,7 @@ Kibana supports spaces in several ways. You can: * <> * <> * <> +* <> * <> [float] @@ -108,6 +109,13 @@ interface. {kib} also has beta <> and <> APIs if you want to automate this process. +[float] +[[spaces-default-route]] +=== Configure a Space-level landing page + +You can create a custom experience for users by configuring the {kib} landing page on a per-space basis. +The landing page can route users to a specific dashboard, application, or saved object as they enter each space. +To configure the landing page, use the `defaultRoute` setting in < Advanced settings>>. [float] [[spaces-delete-started]] diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index e37f08d0c2692..fa583918703f3 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -4,7 +4,7 @@ [partintro] -- *Discover* enables you to explore your data with {kib}'s data discovery functions. -You have access to every document in every index that matches the selected index pattern. +You have access to every document in every index that matches the selected <>. You can submit search queries, filter the search results, and view document data. You can also see the number of documents that match the search query and get field value statistics. If a time field is configured for the selected index pattern, the distribution of diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 72e88ad4634d7..5d35f103ecee0 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -14,7 +14,7 @@ URL**, which is the URL to queue a report for generation. To get the URL for triggering PDF report generation during a given time period: -. Load the saved object in the Visualize editor, or load a Dashboard. +. Load the saved object in *Visualize* or *Dashboard*. . To specify a relative or absolute time period, use the time filter. . In the {kib} toolbar, click *Share*. . Select *PDF Reports*. @@ -22,7 +22,7 @@ To get the URL for triggering PDF report generation during a given time period: To get the URL for triggering CSV report generation during a given time period: -. Load the saved search in Discover. +. Load the saved search in *Discover*. . To specify a relative or absolute time period, use the time filter. . In the {kib} toolbar, click *Share*. . Select *CSV Reports*. diff --git a/docs/user/reporting/images/canvas-share-button.png b/docs/user/reporting/images/canvas-share-button.png new file mode 100644 index 0000000000000..1137a263444ea Binary files /dev/null and b/docs/user/reporting/images/canvas-share-button.png differ diff --git a/docs/user/reporting/images/preserve-layout-switch.png b/docs/user/reporting/images/preserve-layout-switch.png index 9bc60b4e7aaef..9cfbdaafc3ac5 100644 Binary files a/docs/user/reporting/images/preserve-layout-switch.png and b/docs/user/reporting/images/preserve-layout-switch.png differ diff --git a/docs/user/reporting/images/preserve-layout.png b/docs/user/reporting/images/preserve-layout.png deleted file mode 100644 index 0282ea24c13cd..0000000000000 Binary files a/docs/user/reporting/images/preserve-layout.png and /dev/null differ diff --git a/docs/user/reporting/images/print-layout.png b/docs/user/reporting/images/print-layout.png deleted file mode 100644 index c17db4580d266..0000000000000 Binary files a/docs/user/reporting/images/print-layout.png and /dev/null differ diff --git a/docs/user/reporting/images/share-button.png b/docs/user/reporting/images/share-button.png index 178ffa90d790b..46a4cce598119 100644 Binary files a/docs/user/reporting/images/share-button.png and b/docs/user/reporting/images/share-button.png differ diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 4a5ca41ae6be9..06af9e6038445 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -1,70 +1,91 @@ [role="xpack"] -[[xpack-reporting]] +[[reporting-getting-started]] = Reporting from Kibana [partintro] -- -You can generate reports that contain {kib} dashboards, -visualizations, and saved searches. Dashboards and visualizations are -exported as PDF documents, while saved searches in Discover -are exported to CSV. +You can generate a report that contains a {kib} dashboard, visualization, +saved search, or Canvas workpad. Depending on the object type, you can export the data as +a PDF, PNG, or CSV document, which you can keep for yourself, or share with others. -Reporting is located in the share menu from the {kib} toolbar: +Reporting is available from the *Share* menu +in *Discover*, *Visualize*, *Dashboard*, and *Canvas*. [role="screenshot"] image::user/reporting/images/share-button.png["Share"] [float] -== System setup +== Setup {reporting} is automatically enabled in {kib}. The first time {kib} runs, it extracts a custom build for the Chromium web browser, which runs on the server in headless mode to load {kib} and capture the rendered {kib} charts as images. Chromium is an open-source project not related to Elastic, but the Chromium binary for {kib} has been custom-built by Elastic to ensure it works with minimal setup. However, the {kib} server OS might still require additional dependencies for Chromium. See the -<> section for more information about the system dependencies +<> section for more information about the system dependencies for different operating systems. [float] -== Manually generate reports +[[reporting-required-privileges]] +== Roles and privileges -. Open {kib} in your web browser and log in. If you are running {kib} -locally, go to `http://localhost:5601`. To access {kib} and generate -reports, you need the `reporting_user` role, and an additional role with succifient <>, such as the `kibana_user` role. -For more information, see <>. +To generate a report, you must have the `reporting_user` role. You also need +the appropriate {kib} privileges to access the objects that you +want to report on and the {es} indices. See <> +for an example. -. Open the dashboard, visualization, or saved search you want to include -in the report. +[float] +[[manually-generate-reports]] +== Generate a report manually + +. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. + +. In the {kib} toolbar, click *Share*. If you are working in Canvas, +click the share icon image:user/reporting/images/canvas-share-button.png["Canvas Share button"]. -. Click *Share* in the {kib} toolbar: +. Select the option appropriate for your object. You can export: + -[role="screenshot"] -image:user/reporting/images/share-button.png["Reporting Button",link="share-button.png"] +** A dashboard or visualization as either a PNG or PDF document +** A Canvas workpad as a PDF document +** A saved search as a CSV document -. Depending on the {kib} application, choose the appropriate options: +. Generate the report. ++ +A notification appears when the report is complete. -. If you're on Discover, select *CSV Reports*, then click *Generate CSV*. +[float] +[[optimize-pdf]] +== Optimize PDF for print—dashboard only + +By default, {kib} creates a PDF +using the existing layout and size of the dashboard. To create a +printer-friendly PDF with multiple A4 portrait pages and two visualizations +per page, turn on *Optimize for printing*. -. If you're on Visualize or Dashboard: +[role="screenshot"] +image::user/reporting/images/preserve-layout-switch.png["Share"] -.. Select *PDF Reports* -.. Dashboard only: Choose to enable *Optimize for printing* layout mode. For an explanation of the different layout modes, see <>. +[float] +[[manage-report-history]] +== View and manage report history -.. Click *Generate PDF*. +For a list of your reports, go to *Management > Reporting*. +From this view, you can monitor the generation of a report and +download reports that you previously generated. [float] -== Automatically generate reports +[[automatically-generate-reports]] +== Automatically generate a report -If you want to automatically generate reports from a script or with -{watcher}, see <> +To automatically generate a report from a script or with +{watcher}, see <>. -- include::automating-report-generation.asciidoc[] -include::pdf-layout-modes.asciidoc[] include::configuring-reporting.asciidoc[] include::chromium-sandbox.asciidoc[] include::reporting-troubleshooting.asciidoc[] diff --git a/docs/user/reporting/pdf-layout-modes.asciidoc b/docs/user/reporting/pdf-layout-modes.asciidoc deleted file mode 100644 index 7d747ad11c19e..0000000000000 --- a/docs/user/reporting/pdf-layout-modes.asciidoc +++ /dev/null @@ -1,31 +0,0 @@ -[[pdf-layout-modes]] -== PDF layout modes - -When you create a PDF report of a dashboard, you can use the *Optimize PDF for printing* or *Preserve existing layout in PDF* modes. - --- -[role="screenshot"] -image:user/reporting/images/preserve-layout-switch.png["PDF Reporting",link="preserve-layout-switch.png"] --- - -[float] -[[optimize-pdf-for-printing]] -=== Optimize PDF for printing -Create a print friendly PDF with multiple A4 portrait pages and two visualizations per page. - --- -[role="screenshot"] -image:user/reporting/images/print-layout.png["optimize-pdf-for-printing",link="print-layout.png"] --- - -[float] -[[preserve-existing-layout-in-pdf]] -=== Preserve existing layout in PDF -Create a PDF with the existing layout and size of the Visualization or Dashboard. - --- -[role="screenshot"] -image:user/reporting/images/preserve-layout.png["Preserve existing layout in PDF",link="preserve-layout.png"] --- - -When you create a PNG or a PDF report of a visualization, the *Optimize for printing* option is used. diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 17fd51d501d3c..92464c24b45ea 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -7,18 +7,6 @@ Having trouble? Here are solutions to common problems you might encounter while using Reporting. -[float] -=== Verbose logs -{kib} server logs have a lot of useful information for troubleshooting and understanding how things work. If you're having any issues at -all, the full logs from Reporting will be the first place to look. In `kibana.yml`: - -[source,yaml] --------------------------------------------------------------------------------- -logging.verbose: true --------------------------------------------------------------------------------- - -For more information about logging, see <>. - [float] [[reporting-troubleshooting-system-dependencies]] === System dependencies @@ -98,3 +86,28 @@ the CAP_SYS_ADMIN capability. Elastic recommends that you research the feasibility of enabling unprivileged user namespaces before disabling the sandbox. An exception is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. + +[float] +=== Verbose logs +{kib} server logs have a lot of useful information for troubleshooting and understanding how things work. If you're having any issues at +all, the full logs from Reporting will be the first place to look. In `kibana.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +logging.verbose: true +-------------------------------------------------------------------------------- + +For more information about logging, see <>. + +=== Puppeteer debug logs +The Chromium browser that {kib} launches on the server is driven by a NodeJS library for Chromium called Puppeteer. The Puppeteer library +has its own command-line method to generate its own debug logs, which can sometimes be helpful, particularly to figure out if a problem is +caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips + +Using Puppeteer's debug method when launching Kibana would look like: +> Enable verbose logging - internal DevTools protocol traffic will be logged via the debug module under the puppeteer namespace. +> ``` +> env DEBUG="puppeteer:*" ./bin/kibana +> ``` + +The Puppeteer logs are very verbose and could possibly contain sensitive information. Handle the generated output with care. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 4f5f011d41074..4c769c85975c4 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -26,8 +26,8 @@ PUT _watcher/watch/error_report "error_report.pdf" : { "reporting" : { "url": "http://0.0.0.0:5601/api/reporting/generate/printablePdf?jobParams=...", <2> - "retries":6, <3> - "interval":"1s", <4> + "retries":40, <3> + "interval":"15s", <4> "auth":{ <5> "basic":{ "username":"elastic", diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c2b1adc5e1b92..32f341a9c1b7c 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -14,16 +14,17 @@ - <> [[basic-authentication]] -==== Basic Authentication +==== Basic authentication Basic authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. The basic authentication provider uses a Kibana provided login form, and supports authentication using the `Authorization` request header's `Basic` scheme. The session cookies that are issued by the basic authentication provider are stateless. Therefore, logging out of Kibana when using the basic authentication provider clears the session cookies from the browser but does not invalidate the session cookie for reuse. -For more information about basic authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication]. +For more information about basic authentication and built-in users, see +{ref}/setting-up-authentication.html[User authentication]. [[token-authentication]] -==== Token Authentication +==== Token authentication Token authentication allows users to login using the same Kibana provided login form as basic authentication. The token authentication provider is built on {es}'s token APIs. The bearer tokens returned by {es}'s {ref}/security-api-get-token.html[get token API] can be used directly with Kibana using the `Authorization` request header with the `Bearer` scheme. @@ -46,7 +47,7 @@ xpack.security.authc.providers: [token, basic] -------------------------------------------------------------------------------- [[pki-authentication]] -==== Public Key Infrastructure (PKI) Authentication +==== Public key infrastructure (PKI) authentication [IMPORTANT] ============================================================================ @@ -76,9 +77,9 @@ xpack.security.authc.providers: [pki, basic] Note that with `server.ssl.clientAuthentication` set to `required`, users are asked to provide a valid client certificate, even if they want to authenticate with username and password. Depending on the security policies, it may or may not be desired. If not, `server.ssl.clientAuthentication` can be set to `optional`. In this case, {kib} still requests a client certificate, but the client won't be required to present one. The `optional` client authentication mode might also be needed in other cases, for example, when PKI authentication is used in conjunction with Reporting. [[saml]] -==== SAML Single Sign-On +==== SAML single sign-on -SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {xpack-ref}/saml-guide.html[Configuring SAML Single-Sign-On on the Elastic Stack]. +SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. Set the configuration values in `kibana.yml` as follows: @@ -106,7 +107,7 @@ server.xsrf.whitelist: [/api/security/saml/callback] Users will be able to log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. Users who aren't authenticated are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session—users who logged in to a different application using the same Identity Provider in the same browser are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force user to re-authenticate. This login scenario is called _Service Provider initiated login_. [float] -===== SAML and Basic Authentication +===== SAML and basic authentication SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: @@ -135,7 +136,7 @@ xpack.security.authc.saml.maxRedirectURLSize: 1kb -------------------------------------------------------------------------------- [[oidc]] -==== OpenID Connect Single Sign-On +==== OpenID Connect single sign-on Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect should also be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. @@ -166,7 +167,7 @@ server.xsrf.whitelist: [/api/security/v1/oidc] -------------------------------------------------------------------------------- [float] -===== OpenID Connect and Basic Authentication +===== OpenID Connect and basic authentication Similar to SAML, OpenID Connect support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both OpenID Connect and Basic authentication for the same {kib} instance: @@ -179,18 +180,19 @@ xpack.security.authc.providers: [oidc, basic] Users will be able to access the login page and use Basic authentication by navigating to the `/login` URL. [float] -==== Single Sign-On provider details +==== Single sign-on provider details The following sections apply both to <> and <> [float] -===== Access and Refresh Tokens +===== Access and refresh tokens Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider -for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` -setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie -can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged +out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will +automatically renew it with a one-time-use refresh token and store it in the same cookie. {kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and @@ -201,7 +203,7 @@ If {kib} can't redirect the user to the external authentication provider (for ex indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. [float] -===== Local and Global Logout +===== Local and global logout During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been leaked, it can't be re-used after logout. This is known as "local" logout. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 803d22a91a309..2636b3dfc1bd3 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -25,9 +25,6 @@ Open the **Spaces** selection control to specify whether to grant the role acces Use the **Privilege** menu to grant access to features. The default is **Custom**, which you can use to grant access to individual features. Otherwise, you can grant read and write access to all current and future features by selecting **All**, or grant read access to all current and future features by selecting **Read**. -[IMPORTANT] -If a feature is hidden using the Spaces disabled features, it will remain hidden even if the user has the necessary privileges. - When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to Kibana, roles that use the custom option do not automatically get access to the new features. You must manually update the roles. NOTE: Machine Learning and Stack Monitoring rely on built-in roles to grant access. When a user is assigned the appropriate roles, the Machine Learning and Stack Monitoring application are available; otherwise, these applications are not visible. @@ -38,6 +35,30 @@ To apply your changes, click **Create space privilege**. The space privilege sho [role="screenshot"] image::user/security/images/create-space-privilege.png[Create space privilege] +==== Feature availability + +Features are available to users when their roles grant access to the features, **and** those features are visible in their current space. The following matrix explains when features are available to users when controlling access via <> and role-based access control: + +|=== +|**Spaces config** |**Role config** |**Result** + +|Feature hidden +|Feature disabled +|Feature not available + +|Feature hidden +|Feature enabled +|Feature not available + +|Feature visible +|Feature disabled +|Feature not available + +|Feature visible +|Feature enabled +|**Feature available** +|=== + ==== Assigning different privileges to different spaces Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added space privileges, click **Add space privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 1d7a3f4978ee0..aaba60ca4b3ca 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -8,18 +8,19 @@ user actions in {kib}. To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with -{xpack-ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} +{ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. For more information, see <>. [[reporting-app-users]] -To enable users to generate reports, assign them the built in `reporting_user` -and `kibana_user` roles: +To enable users to generate reports, assign them the built-in `reporting_user` +role. Users will also need the appropriate <> to access the objects +to report on and the {es} indices. * If you're using the `native` realm, you can assign roles through -**Management / Users** UI in Kibana or with the `user` API. For example, +**Management > Users** UI in Kibana or with the `user` API. For example, the following request creates a `reporter` user that has the -`reporting_user` role, and another role with sufficient <>, such as the `kibana_user` role: +`reporting_user` role and the `kibana_user` role: + [source, sh] --------------------------------------------------------------- @@ -34,7 +35,7 @@ POST /_security/user/reporter * If you are using an LDAP or Active Directory realm, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in -{xpack-ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. +{ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: + @@ -54,7 +55,7 @@ In a production environment, you should restrict access to the {reporting} endpoints to authorized users. This requires that you: . Enable {security} on your {es} cluster. For more information, -see {xpack-ref}/security-getting-started.html[Getting Started with Security]. +see {ref}/security-getting-started.html[Getting Started with Security]. . Configure an SSL certificate for Kibana. For more information, see <>. . Configure {watcher} to trust the Kibana server's certificate by adding it to @@ -82,4 +83,4 @@ includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{xpack-ref}/how-watcher-works.html[How Watcher Works]. +{ref}/how-watcher-works.html[How Watcher works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 1c74bd98642a7..60f5473f43b9d 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters" For more information, see <>. -- -. Optional: Change the default session duration. By default, sessions stay -active until the browser is closed. To change the duration, set the -`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file. -The timeout is specified in milliseconds. For example, set the timeout to 600000 -to expire sessions after 10 minutes: +. Optional: Set a timeout to expire idle sessions. By default, a session stays +active until the browser is closed. To define a sliding session expiration, set +the `xpack.security.session.idleTimeout` property in the `kibana.yml` +configuration file. The idle timeout is specified in milliseconds. For example, +set the idle timeout to 600000 to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.sessionTimeout: 600000 +xpack.security.session.idleTimeout: 600000 +-------------------------------------------------------------------------------- +-- + +. Optional: Change the maximum session duration or "lifespan" -- also known as +the "absolute timeout". By default, a session stays active until the browser is +closed. If an idle timeout is defined, a session can still be extended +indefinitely. To define a maximum session lifespan, set the +`xpack.security.session.lifespan` property in the `kibana.yml` configuration +file. The lifespan is specified in milliseconds. For example, set the lifespan +to 28800000 to expire sessions after 8 hours: ++ +-- +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.session.lifespan: 28800000 -------------------------------------------------------------------------------- -- @@ -106,7 +121,7 @@ TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges on specific index patterns. For more information, see -{xpack-ref}/authorization.html[Configuring Role-based Access Control]. +{ref}/authorization.html[User authorization]. -- diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index ed74525d22e7c..e69d62daf7435 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -3,58 +3,49 @@ [partintro] -- -_Visualize_ enables you to create visualizations of the data in your -Elasticsearch indices. You can then build <> that -display related visualizations. +_Visualize_ enables you to create visualizations of the data from your Elasticsearch indices, which you can then add to dashboards for analysis. -Kibana visualizations are based on Elasticsearch queries. By using a -series of Elasticsearch {ref}/search-aggregations.html[aggregations] -to extract and process your data, you can create charts that show -you the trends, spikes, and dips you need to know about. +{kib} visualizations are based on Elasticsearch queries. By using a series of {es} {ref}/search-aggregations.html[aggregations] to extract and process your data, you can create charts that show you the trends, spikes, and dips you need to know about. -You can create visualizations from a search saved from <> -or start with a new search query. --- - -[[createvis]] -== Creating a Visualization +[float] +[[create-a-visualization]] +== Create visualizations -To create a visualization: - -. Click on *Visualize* in the side navigation. -. Click the *Create new visualization* button or the **+** button. +. Open *Visualize*. +. Click *Create new visualization*. . Choose the visualization type: - ++ * *Basic charts* -[horizontal] <>:: Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display. -<>:: Compare different series in X/Y charts. -<>:: Shade cells within a matrix. -<>:: Display each source's contribution to a total. -* *Data* +* *<>* [horizontal] -<>:: Display the raw data of a composed aggregation. -<>:: Display a single number. -<>:: Display a gauge. -* *Maps* -[horizontal] -<>:: Associate the results of an aggregation with geographic locations. -<>:: Thematic maps where a shape's color intensity corresponds to a metric's value. -locations. -* *Time Series* +Line, area, and bar charts:: Compare different series in X/Y charts. +Pie chart:: Display each source contribution to a total. +Data table:: Flattens aggregations into table format. +Metric:: Display a single number. +Goal and gauge:: Display a number with progress indicators. +Heat maps:: Display shaded cells within a matrix. +Tag cloud:: Display words in a cloud, where the size of the word corresponds to its importance. +* *Time series optimized* [horizontal] +<>:: Visualize time series data using pipeline aggregations. <>:: Compute and combine data from multiple time series data sets. -<>:: Visualize time series data using pipeline aggregations. -* *Other* +* *Maps* +[horizontal] +<>:: The most powerful way of visualizing map data in {kib}. +<>:: Displays points on a map using a geohash aggregation. +<>:: Merge any structured map data onto a shape. +* *<>* +[horizontal] +<>:: Provides the ability to add interactive inputs to a Dashboard. +<>:: Display free-form information or instructions. +* *For developers* [horizontal] -<>:: Controls provide the ability to add interactive inputs to Kibana Dashboards. -<>:: Display free-form information or -instructions. -<>:: Display words as a cloud in which the size of the word correspond to its importance. -<>:: Support for user-defined graphs, external data sources, images, and user-defined interactivity. +<>:: Complete control over query and display. + . Specify a search query to retrieve the data for your visualization: -** To enter new search criteria, select the index pattern for the indices that +** To enter new search criteria, select the <> for the indices that contain the data you want to visualize. This opens the visualization builder with a wildcard query that matches all of the documents in the selected indices. @@ -67,110 +58,23 @@ modifications to the saved search are automatically reflected in the visualization. To disable automatic updates, you can disconnect a visualization from the saved search. -. In the visualization builder, choose the metric aggregation for the -visualization's Y axis: - -* *Metric Aggregations*: - -* {ref}/search-aggregations-metrics-valuecount-aggregation.html[count] -* {ref}/search-aggregations-metrics-avg-aggregation.html[average] -* {ref}/search-aggregations-metrics-sum-aggregation.html[sum] -* {ref}/search-aggregations-metrics-min-aggregation.html[min] -* {ref}/search-aggregations-metrics-max-aggregation.html[max] -* {ref}/search-aggregations-metrics-stats-aggregation.html[standard deviation] -* {ref}/search-aggregations-metrics-cardinality-aggregation.html[unique count] -* {ref}/search-aggregations-metrics-percentile-aggregation.html[median] (50th percentile) -* {ref}/search-aggregations-metrics-percentile-aggregation.html[percentiles] -* {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[percentile ranks] -* {ref}/search-aggregations-metrics-top-hits-aggregation.html[top hit] -* {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geo centroid] - - -* *Parent Pipeline Aggregations*: - -* {ref}/search-aggregations-pipeline-derivative-aggregation.html[derivative] -* {ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[cumulative sum] -* {ref}/search-aggregations-pipeline-movavg-aggregation.html[moving average] -* {ref}/search-aggregations-pipeline-serialdiff-aggregation.html[serial diff] - - -* *Sibling Pipeline Aggregations*: - -* {ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[average bucket] -* {ref}/search-aggregations-pipeline-sum-bucket-aggregation.html[sum bucket] -* {ref}/search-aggregations-pipeline-min-bucket-aggregation.html[min bucket] -* {ref}/search-aggregations-pipeline-max-bucket-aggregation.html[max bucket] - - -. For the visualizations X axis, select a bucket aggregation: -+ -* {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] -* {ref}/search-aggregations-bucket-range-aggregation.html[range] -* {ref}/search-aggregations-bucket-terms-aggregation.html[terms] -* {ref}/search-aggregations-bucket-filters-aggregation.html[filters] -* {ref}/search-aggregations-bucket-significantterms-aggregation.html[significant terms] - -For example, if you're indexing Apache server logs, you could build bar chart -that shows the distribution of incoming requests by geographic location by -specifying a terms aggregation on the `geo.src` field: - -image::images/bar-terms-agg.jpg[] - -The y-axis shows the number of requests received from each country, and the -countries are displayed across the x-axis. - -Bar, line, or area chart visualizations use _metrics_ for the y-axis and -_buckets_ for the x-axis. Buckets are analogous to SQL `GROUP BY` -statements. Pie charts, use the metric for the slice size and the bucket -for the number of slices. - -You can further break down the data by specifying sub aggregations. The first -aggregation determines the data set for any subsequent aggregations. Sub -aggregations are applied in order--you can drag the aggregations to change the -order in which they're applied. - -For example, you could add a terms sub aggregation on the `geo.dest` field to -the Country of Origin bar chart to see the locations those requests were -targeting. - -image::images/bar-terms-subagg.jpg[] - -For more information about working with sub aggregations, see -https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[Kibana, -Aggregation Execution Order, and You]. - -include::{kib-repo-dir}/visualize/saving.asciidoc[] - +-- include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[] include::{kib-repo-dir}/visualize/lens.asciidoc[] -include::{kib-repo-dir}/visualize/xychart.asciidoc[] - -include::{kib-repo-dir}/visualize/controls.asciidoc[] - -include::{kib-repo-dir}/visualize/datatable.asciidoc[] +include::{kib-repo-dir}/visualize/most-frequent.asciidoc[] -include::{kib-repo-dir}/visualize/markdown.asciidoc[] - -include::{kib-repo-dir}/visualize/metric.asciidoc[] - -include::{kib-repo-dir}/visualize/goal.asciidoc[] - -include::{kib-repo-dir}/visualize/pie.asciidoc[] +include::{kib-repo-dir}/visualize/tsvb.asciidoc[] +include::{kib-repo-dir}/visualize/timelion.asciidoc[] include::{kib-repo-dir}/visualize/tilemap.asciidoc[] - include::{kib-repo-dir}/visualize/regionmap.asciidoc[] -include::{kib-repo-dir}/visualize/timelion.asciidoc[] - -include::{kib-repo-dir}/visualize/tsvb.asciidoc[] - -include::{kib-repo-dir}/visualize/tagcloud.asciidoc[] - -include::{kib-repo-dir}/visualize/heatmap.asciidoc[] +include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] include::{kib-repo-dir}/visualize/vega.asciidoc[] +include::{kib-repo-dir}/visualize/saving.asciidoc[] + include::{kib-repo-dir}/visualize/inspector.asciidoc[] diff --git a/docs/visualize/aggregations.asciidoc b/docs/visualize/aggregations.asciidoc new file mode 100644 index 0000000000000..36ddb0063dfc3 --- /dev/null +++ b/docs/visualize/aggregations.asciidoc @@ -0,0 +1,136 @@ +[[supported-aggregations]] +=== Supported aggregations + +The most frequently used visualizations support the following aggregations. + +[float] +[[visualize-metric-aggregations]] +==== Metric aggregations + +The *Count* metric lets you visualize the number of documents in a bucket. +If there are no bucket aggregations defined, this is the total number of documents that match the query. +It is the default selection. + +All other metric aggregations require a field selection, which will read from the indexed values. Alternatively, +you can override field values with a script using the <>. The +other metric aggregations are: + +{ref}/search-aggregations-metrics-avg-aggregation.html[Average]:: The mean value. +{ref}/search-aggregations-metrics-max-aggregation.html[Maximum]:: The highest value. +{ref}/search-aggregations-metrics-percentile-aggregation.html[Median]:: The value that is in the 50% percentile. +{ref}/search-aggregations-metrics-min-aggregation.html[Minimum]:: The lowest value. +{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: The total value. + +Unique Count:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] of the field within the bucket. +Supports any data type. + +Standard Deviation:: Requires a numeric field. Uses the {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation. + +{ref}/search-aggregations-metrics-top-hits-aggregation.html[Top Hit]:: Returns a sample of individual documents. When the Top Hit aggregation is matched to more than one document, you must choose a technique for combining the values. Techniques include average, minimum, maximum, and sum. + +{ref}/search-aggregations-metrics-percentile-aggregation.html[Percentiles]:: Divides the +values in a numeric field into specified percentile bands. Select a field from the drop-down, then specify one or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a percentile field. + +{ref}/search-aggregations-metrics-percentile-rank-aggregation.html[Percentile Rank]:: Returns the percentile rankings for the values in the specified numeric field. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a values field. Click *+Add* to add a values field. + +[float] +[[visualize-sibling-pipeline-aggregations]] +==== Sibling pipeline aggregations + +For each of the sibling pipeline aggregations you have to define a bucket and metric to calculate. This +has the effect of condensing many buckets into one number. + +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Average Bucket]:: Calculates the mean, or average, value of a specified metric in a sibling aggregation. + +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Sum Bucket]:: Calculates the sum of the values of a specified metric in a sibling aggregation. + +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Min Bucket]:: Calculates the minimum value of a specified metric in a sibling aggregation. + +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Max Bucket]:: Calculates the maximum value of a specified metric in a sibling aggregation. + +[float] +[[visualize-bucket-aggregations]] +==== Bucket aggregations + +{ref}/search-aggregations-bucket-datehistogram-aggregation.html[Date Histogram]:: Splits a date field into buckets by interval. If the date field is the primary time field for the index pattern, it will pick an automatic interval for you. You can also choose a minimum time interval, or specify a custom interval frame by selecting *Custom* as the interval and +specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, +*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, +down to one millisecond. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch.For example, the tooltip for a monthly interval will show the first day of the month. + +{ref}/search-aggregations-bucket-histogram-aggregation.html[Histogram]:: Builds from a numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. + +{ref}/search-aggregations-bucket-range-aggregation.html[Range]:: Specify ranges of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. + +{ref}/search-aggregations-bucket-daterange-aggregation.html[Date Range]:: Reports values that are within a range of dates that you specify. You can specify the ranges for the dates using {ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. +Click the red *(x)* symbol to remove a range. + +{ref}/search-aggregations-bucket-iprange-aggregation.html[IPv4 Range]:: Specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. + +*Filters*:: Each filter creates a bucket of documents. You can specify a filter as a +<> or <> query string. Click *Add Filter* to +add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where +you can type in a name to display on the visualization. + +{ref}/search-aggregations-bucket-terms-aggregation.html[Terms]:: Specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. + +{ref}/search-aggregations-bucket-significantterms-aggregation.html[Significant Terms]:: Returns interesting or unusual occurrences of terms in a set. + +Both Terms and Significant Terms support {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns] which +are available by clicking *Advanced* after selecting a field. + +Kibana only supports filtering string fields with regular expression patterns, it does not support matching with arrays or filtering numeric fields. +Patterns are case sensitive. + +Example: + +* You want to exclude the metricbeat process from your visualization of top processes: `metricbeat.*` +* You only want to show processes collecting beats: `.*beat` +* You want to exclude two specific values, the string `"empty"` and `"none"`: `empty|none` + +*Geo aggregations* + +These are only supported by the tile map and table visualizations: + +{ref}/search-aggregations-bucket-geohashgrid-aggregation.html[Geohash]:: Displays points based on a geohash. + +{ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile]:: Groups points based on web map tiling. + + +[float] +[[visualize-parent-pipeline-aggregations]] +==== Parent pipeline aggregations + +For each of the parent pipeline aggregations you have to define a bucket and metric to calculate. These +metrics expect the buckets to be ordered, and are especially useful for time series data. +You can also nest these aggregations. For example, if you want to produce a third derivative. + +These visualizations support parent pipeline aggregations: + +* Line, Area and Bar charts +* Data table + +{ref}/search-aggregations-pipeline-derivative-aggregation.html[Derivative]:: Calculates the derivative of specific metrics. + +{ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[Cumulative Sum]:: Calculates the cumulative sum of a specified metric in a parent histogram. + +{ref}/search-aggregations-pipeline-movavg-aggregation.html[Moving Average]:: Slides a window across the data and emits the average value of the window. + +{ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial Diff]:: Values in a time series are subtracted from itself at different time lags or periods. + +Custom {kib} plugins can <>, which includes support for adding more aggregations. + +[float] +[[visualize-advanced-aggregation-options]] +==== Advanced aggregation options + +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +definition, as in the following example: + +[source,shell] +{ "script" : "doc['grade'].value * 1.2" } + +This example implements a {es} {ref}/search-aggregations.html[Script Value Source] which replaces +the value in the metric. The availability of these options varies depending on the aggregation +you choose. + +When multiple bucket aggregations are defined, you can use the drag target on each aggregation to change the priority. For more information about working with aggregation order, see https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[Kibana, Aggregation Execution Order, and You]. diff --git a/docs/visualize/controls.asciidoc b/docs/visualize/controls.asciidoc deleted file mode 100644 index f138044d788ef..0000000000000 --- a/docs/visualize/controls.asciidoc +++ /dev/null @@ -1,95 +0,0 @@ -[[controls]] -== Controls Visualization -experimental[] - - -The Controls visualization enables you to add interactive inputs -to Kibana dashboards. You can create two types of inputs: -a dropdown menu and a radio slider. - -[role="screenshot"] -image::images/controls/controls_in_dashboard.png[] - -[[add-input-controls]] -=== Adding Input Controls - -To start a *Controls* visualization, open the Visualization application -and click the *+* button. Scroll to the *Others* section and -select *Controls*. - -In the visualization builder, choose the type of control to add to -your visualization. - -==== Dropdown menu - -A dropdown menu allows users to filter content by selecting -one or more options from a list. The dropdown menu is dynamically populated -with the results of a terms aggregation. - -[role="screenshot"] -image::images/controls/dropdown_control_editor.png[] - -*Control Label*:: The label for the dropdown menu. By default, the -label is the field name. - -*Index Pattern*:: The <> that contains -the data set to visualize. - -*Field*:: The field used to populate the list of options -and filter on when users interact with the input. -The list of available fields is derived from the specified -index pattern. - -*Parent control*:: The control for chaining dropdown menus so that the -selection in the first menu -filters the terms in the second menu. Only available when -creating multiple dropdown menus. - -*Multiselect*:: When enabled, the dropdown menu allows users to select multiple options. - -*Size*:: The number of options to include in the list. - -==== Range slider - -A range sliders allow users to filter content within a range of numbers. -The range slider minimum and maximum values are dynamically populated with -the results of a min and max aggregation. - -[role="screenshot"] -image::images/controls/range_slider_editor.png[] - -*Control Label*:: The label for the range slider. By default, the -label is the field name. - -*Index Pattern*:: The <> that contains -the data set to visualize. - -*Field*:: The field used to populate the range slider -and filter on when users interact with the input. -The list of available fields is derived from the -specified index pattern. - -*Step Size*:: The increment/decrement size of the slider. - -*Decimal Places*:: The number of decimal places. - -[[global-options]] -=== Global Options - -Open the *Options* tab to configure settings that apply to all input -controls in a Controls visualization. - -[role="screenshot"] -image::images/controls/controls_options.png[] - -*Update Kibana filters on each change*:: When enabled, all input interactions -immediately create filters that cause the dashboard to refresh. When disabled, -Kibana filters are only created -when the user clicks *Apply changes* image:images/apply-changes-button.png[]. - -*Use time filter*:: When enabled, the aggregations used to generate -the dropdown options list and range minimum and maximum are bound -to <>. - -*Pin filters to global state*:: When enabled, all filters created by -interacting with the inputs are automatically pinned. diff --git a/docs/visualize/datatable.asciidoc b/docs/visualize/datatable.asciidoc deleted file mode 100644 index 7a65b8cdb5fab..0000000000000 --- a/docs/visualize/datatable.asciidoc +++ /dev/null @@ -1,75 +0,0 @@ -[[data-table]] -== Data Table - -include::y-axis-aggs.asciidoc[] - -The rows of the data table are called _buckets_. You can define buckets to split the table into rows or to split -the table into additional tables. - -Each bucket type supports the following aggregations: - -*Date Histogram*:: A {ref}/search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, -down to one second. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch. -For example, the tooltip for a monthly interval will show the first day of the month. -*Histogram*:: A standard {ref}/search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty -intervals in the histogram. -*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove -a range. -*Date Range*:: A {ref}/search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. -Click the red *(/)* symbol to remove a range. -*IPv4 Range*:: The {ref}/search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to -remove a range. -*Terms*:: A {ref}/search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top -or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type -in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the -*Size* parameter defines the number of entries this aggregation returns. -*Geohash*:: The {ref}/search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation displays points -based on the geohash coordinates. - -Once you've specified a bucket type aggregation, you can define sub-buckets to refine the visualization. Click -*+ Add sub-buckets* to define a sub-bucket, then choose *Split Rows* or *Split Table*, then select an -aggregation from the list of types. - -You can use the up or down arrows to the right of the aggregation's type to change the aggregation's priority. - -Enter a string in the *Custom Label* field to change the display label. - -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*Exclude Pattern*:: Specify a pattern in this field to exclude from the results. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* tab to change the following aspects of the table: - -*Per Page*:: This field controls the pagination of the table. The default value is ten rows per page. -*Show metrics for every bucket/level*:: Check this box to display the intermediate results for each bucket aggregation. -*Show partial rows*:: Check this box to display a row even when there is no result. -*Show total*:: Check this box to display a row at the bottom of the table with each column's total value. -*Total function*:: This field controls the function used to calculate totals that you can toggle with the **Show total** checkbox. -*Percentage column*:: Select a column to add a percentage based column on the same data. - -NOTE: Enabling these behaviors may have a substantial effect on performance. diff --git a/docs/visualize/for-dashboard.asciidoc b/docs/visualize/for-dashboard.asciidoc new file mode 100644 index 0000000000000..a197998ecdc9d --- /dev/null +++ b/docs/visualize/for-dashboard.asciidoc @@ -0,0 +1,117 @@ +[[for-dashboard]] +== Markdown and controls + +[float] +[[markdown-widget]] +=== Markdown widget + +The Markdown widget is a text entry field that accepts GitHub-flavored Markdown text. Kibana renders the text you enter +in this field and displays the results on the dashboard. You can click the *Help* link to go to the +https://help.github.com/articles/github-flavored-markdown/[help page] for GitHub flavored Markdown. From the widget +you can: + +* Click *Apply* to display the rendered text in the Preview panel +* Click *Discard* to revert to a previously saved version + + +[float] +[[controls]] +=== Controls widget +experimental[] + +The Controls widget enables you to add interactive inputs +to a dashboard. You can create two types of inputs: + +* Dropdown menu +* Radio slider + +[role="screenshot"] +image::images/controls/controls_in_dashboard.png[] + +[float] +[[add-input-controls]] +=== Add input controls + +To start a *Controls* visualization, open the Visualization application +and click the *+* button. Scroll to the *Others* section and +select *Controls*. + +In the visualization builder, choose the type of control to add to +your visualization. + +[float] +==== Dropdown menu + +A dropdown menu allows users to filter content by selecting +one or more options from a list. The dropdown menu is dynamically populated +with the results of a terms aggregation. + +[role="screenshot"] +image::images/controls/dropdown_control_editor.png[] + +*Control Label*:: The label for the dropdown menu. By default, the +label is the field name. + +*Index Pattern*:: The <> that contains +the data set to visualize. + +*Field*:: The field used to populate the list of options +and filter on when users interact with the input. +The list of available fields is derived from the specified +index pattern. + +*Parent control*:: The control for chaining dropdown menus so that the +selection in the first menu +filters the terms in the second menu. Only available when +creating multiple dropdown menus. + +*Multiselect*:: When enabled, the dropdown menu allows users to select multiple options. + +*Size*:: The number of options to include in the list. + +[float] +==== Range slider + +A range sliders allow users to filter content within a range of numbers. +The range slider minimum and maximum values are dynamically populated with +the results of a min and max aggregation. + +[role="screenshot"] +image::images/controls/range_slider_editor.png[] + +*Control Label*:: The label for the range slider. By default, the +label is the field name. + +*Index Pattern*:: The <> that contains +the data set to visualize. + +*Field*:: The field used to populate the range slider +and filter on when users interact with the input. +The list of available fields is derived from the +specified index pattern. + +*Step Size*:: The increment/decrement size of the slider. + +*Decimal Places*:: The number of decimal places. + +[float] +[[global-options]] +=== Global options + +Open the *Options* tab to configure settings that apply to all input +controls in a Controls visualization. + +[role="screenshot"] +image::images/controls/controls_options.png[] + +*Update Kibana filters on each change*:: When enabled, all input interactions +immediately create filters that cause the dashboard to refresh. When disabled, +Kibana filters are only created +when the user clicks *Apply changes* image:images/apply-changes-button.png[]. + +*Use time filter*:: When enabled, the aggregations used to generate +the dropdown options list and range minimum and maximum are bound +to <>. + +*Pin filters to global state*:: When enabled, all filters created by +interacting with the inputs are automatically pinned. diff --git a/docs/visualize/goal.asciidoc b/docs/visualize/goal.asciidoc deleted file mode 100644 index a725494117bd1..0000000000000 --- a/docs/visualize/goal.asciidoc +++ /dev/null @@ -1,38 +0,0 @@ -[[goal-chart]] -== Goal and Gauge - -A goal visualization displays how your metric progresses toward a fixed goal. A gauge visualization displays in which -predefined range falls your metric. - -include::y-axis-aggs.asciidoc[] - -Open the *Advanced* link to display more customization options: - -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Click the *Options* tab to change the following options: - -* *Gauge Type* select between arc, circle and metric display type. -* *Percentage Mode* will show all values as percentages -* *Vertical Split* will put the gauges one under another instead of one next to another -* *Show Labels* selects whether you want to show or hide the labels -* *Sub Text* text for the label that appears below the value -* *Auto Extend Range* automatically grows the gauge if value is over its extents. -* *Ranges* you can add custom ranges. Each range will get assigned a color. If value falls within that range it will get -assigned that color. -** A chart with a single range is called a *goal* chart. -** A chart with multiple ranges is called a *gauge* chart. Gauge charts are initialized with a predefined set of ranges. Adjust the ranges to fit the need of your data set and use case. -** *Caution:* Field formatters can be applied to the displayed value causing the range values and the displayed values to be different. For example: The _bytes_ field formatter applied to the Metrics field will have displayed values like "30MB". The raw value is really closer to 30,000,000. You will need to set your range values to the raw value and not the formatted value. -* *Color Options* define how to color your ranges (which color schema to use). Color options are only visible if more than -one range is defined. -* *Style - Show Scale* shows or hides the scale -* *Style - Color Labels* whether the labels should have the same color as the range where the value falls in diff --git a/docs/visualize/heatmap.asciidoc b/docs/visualize/heatmap.asciidoc deleted file mode 100644 index a8fd71a160d32..0000000000000 --- a/docs/visualize/heatmap.asciidoc +++ /dev/null @@ -1,81 +0,0 @@ -[[heatmap-chart]] -== Heatmap Chart - -A heat map is a graphical representation of data where the individual values contained in a matrix are represented as colors. -The color for each matrix position is determined by the _metrics_ aggregation. The following aggregations are available for -this chart: - -include::y-axis-aggs.asciidoc[] - -The _buckets_ aggregations determine what information is being retrieved from your data set. - -Before you choose a buckets aggregation, specify if you are defining buckets for X or Y axis within a single chart -or splitting into multiple charts. A multiple chart split must run before any other aggregations. -When you split a chart, you can change if the splits are displayed in a row or a column by clicking -the *Rows | Columns* selector. - -This chart's X and Y axis supports the following aggregations. Click the linked name of each aggregation to visit the main -Elasticsearch documentation for that aggregation. - -*Date Histogram*:: A {ref}/search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, -down to one second. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch. -For example, the tooltip for a monthly interval will show the first day of the month. - -*Histogram*:: A standard {ref}/search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty -intervals in the histogram. -*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove -a range. -*Date Range*:: A {ref}/search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. -Click the red *(x)* symbol to remove a range. -*IPv4 Range*:: The {ref}/search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to -remove a range. -*Terms*:: A {ref}/search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top -or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where -you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. - -Enter a string in the *Custom Label* field to change the display label. - -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*Exclude Pattern*:: Specify a pattern in this field to exclude from the results. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* tab to change the following aspects of the chart: - -*Show Tooltips*:: Check this box to enable the display of tooltips. -*Highlight*:: Check this box to enable highlighting of elements with same label -*Legend Position*:: You can select where to display the legend (top, left, right, bottom) - - -*Color Schema*:: You can select an existing color schema or go for custom and define your own colors in the legend -*Reverse Color Schema*:: Checking this checkbox will reverse the color schema. -*Color Scale*:: You can switch between linear, log and sqrt scales for color scale. -*Scale to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check -this box to change both upper and lower bounds to match the values returned in the data. -*Number of Colors*:: Number of color buckets to create. Minimum is 2 and maximum is 10. -*Percentage Mode*:: Enabling this will show legend values as percentages. -*Custom Range*:: You can define custom ranges for your color buckets. For each of the color bucket you need to specify -the minimum value (inclusive) and the maximum value (exclusive) of a range. -*Show Label*:: Enables showing labels with cell values in each cell - *Rotate*:: Allows rotating the cell value label by 90 degrees. diff --git a/docs/visualize/inspector.asciidoc b/docs/visualize/inspector.asciidoc index 923d9e601e876..ed98daea211e1 100644 --- a/docs/visualize/inspector.asciidoc +++ b/docs/visualize/inspector.asciidoc @@ -1,19 +1,11 @@ [[vis-inspector]] -== Inspecting Visualizations +== Inspect visualizations -Many visualizations allow you to inspect the data behind the -visualization. +Many visualizations allow you to inspect the query and data behind the visualization. -To inspect a visualization, click the *Inspect* button in the editor or -select *Inspect* from the Dashboard panel menu. - -The initial view shows the underlying data for the visualization. You can -download the data as a comma separated values (CSV) file in -*Formatted* or *Raw* format. Formatted downloads the data in table format. -Raw downloads the data as provided -- dates are timestamps, numbers don’t have -thousand separators, and so on. - -To view the requests that collected the data, select *Requests* from the *View* -menu in the upper right. - -Which views are available depends on the inspected visualization. +. In the {kib} toolbar, click *Inspect*. +. To download the data, click *Download CSV*, then choose one of the following options: +* *Formatted CSV* - Downloads the data in table format. +* *Raw CSV* - Downloads the data as provided. +. To view the data collection requests, select *Requests* from the *View* +dropdown. diff --git a/docs/visualize/markdown.asciidoc b/docs/visualize/markdown.asciidoc deleted file mode 100644 index e4542c8cdd2dd..0000000000000 --- a/docs/visualize/markdown.asciidoc +++ /dev/null @@ -1,7 +0,0 @@ -[[markdown-widget]] -== Markdown Widget - -The Markdown widget is a text entry field that accepts GitHub-flavored Markdown text. Kibana renders the text you enter -in this field and displays the results on the dashboard. You can click the *Help* link to go to the -https://help.github.com/articles/github-flavored-markdown/[help page] for GitHub flavored Markdown. Click *Apply* to -display the rendered text in the Preview pane or *Discard* to revert to a previous version. diff --git a/docs/visualize/metric.asciidoc b/docs/visualize/metric.asciidoc index 4cb29555eea77..9cbc4a0f7a550 100644 --- a/docs/visualize/metric.asciidoc +++ b/docs/visualize/metric.asciidoc @@ -1,21 +1,4 @@ [[metric-chart]] -== Metric - -A metric visualization displays a single number for each aggregation you select: - -include::y-axis-aggs.asciidoc[] - -You can click the *Advanced* link to display more customization options: - -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. +=== Metric Click the *Options* tab to display the font size slider. diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc new file mode 100644 index 0000000000000..7452f1c4c3d7e --- /dev/null +++ b/docs/visualize/most-frequent.asciidoc @@ -0,0 +1,63 @@ +[[most-frequent]] +== Most frequently used visualizations + +The most frequently used visualizations allow you to plot aggregated data from a <> or <>. They all support a single level of +Elasticsearch {es} {ref}/search-aggregations-metrics.html[metric] aggregations, and one or more +levels of {es} {ref}/search-aggregations-bucket.html[bucket] aggregations. + +The most frequently used visualizations include: + +* Line, Area and Bar charts +* Pie charts +* Data table +* Metric visualization +* Goal and Gauge visualization +* Heat maps +* Tag cloud + +[float] +=== Configure your visualization + +You configure visualizations using the default editor, which is broken into *Metrics* and *Buckets*, and includes a default count +metric. Each visualization supports different configurations for what the metrics and buckets +represent. For example, a Bar chart allows you to add an X-axis: + +[role="screenshot"] +image::images/add-bucket.png["",height=478] + +A common configuration for the X-axis is to use a {es} {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] aggregation: + +[role="screenshot"] +image::images/visualize-date-histogram.png[] + +To see your changes, click *Apply changes* image:images/apply-changes-button.png[] + +If it's supported by the visualization, you can add more buckets. In this example we have +added a +{es} {ref}/search-aggregations-bucket-terms-aggregation.html[terms] aggregation on the field +`geo.src` to show the top 5 sources of log traffic. + +[role="screenshot"] +image::images/visualize-date-histogram-split-1.png[] + +The new aggregation is added after the first one, so the result shows +the top 5 sources of traffic per 3 hours. If you want to change the aggregation order, you can do +so by dragging: + +[role="screenshot"] +image::images/visualize-drag-reorder.png["",width=366] + +The visualization +now shows the top 5 sources of traffic overall, and compares them in 3 hour increments: + +[role="screenshot"] +image::images/visualize-date-histogram-split-2.png[] + +For more information about how aggregations are used in visualizations, see <>. + +Each visualization also has its own customization options. Most visualizations allow you to customize the color of a specific series: + +[role="screenshot"] +image::images/color-picker.png[An array of color dots that users can select,height=267] + +include::aggregations.asciidoc[] diff --git a/docs/visualize/pie.asciidoc b/docs/visualize/pie.asciidoc deleted file mode 100644 index edaf97291cf4c..0000000000000 --- a/docs/visualize/pie.asciidoc +++ /dev/null @@ -1,86 +0,0 @@ -[[pie-chart]] -== Pie Charts - -The slice size of a pie chart is determined by the _metrics_ aggregation. The following aggregations are available for -this axis: - -*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of -the elements in the selected index pattern. -*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric -field. Select a field from the drop-down. -*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns -the number of unique values in a field. Select a field from the drop-down. - -Enter a string in the *Custom Label* field to change the display label. - -The _buckets_ aggregations determine what information is being retrieved from your data set. - -Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into -multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change -if the splits are displayed in a row or a column by clicking the *Rows | Columns* selector. - -You can specify any of the following bucket aggregations for your pie chart: - -*Date Histogram*:: A {ref}/search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, -down to one second. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch. -For example, the tooltip for a monthly interval will show the first day of the month. -*Histogram*:: A standard {ref}/search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty -intervals in the histogram. -*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove -a range. -*Date Range*:: A {ref}/search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. -Click the red *(/)* symbol to remove a range. -*IPv4 Range*:: The {ref}/search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(/)* symbol to -remove a range. -*Terms*:: A {ref}/search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top -or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[] *label* button to open the label field, where you can type -in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. The value of the -*Size* parameter defines the number of entries this aggregation returns. - -After defining an initial bucket aggregation, you can define sub-buckets to refine the visualization. Click *+ Add -sub-buckets* to define a sub-aggregation, then choose *Split Slices* to select a sub-bucket from the list of -types. - -When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the -aggregation's type to change the aggregation's priority. - -include::color-picker.asciidoc[] - -Enter a string in the *Custom Label* field to change the display label. - -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*Exclude Pattern*:: Specify a pattern in this field to exclude from the results. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* tab to change the following aspects of the table: - -*Donut*:: Display the chart as a sliced ring instead of a sliced pie. -*Show Tooltip*:: Check this box to enable the display of tooltips. - -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard -changes* button to keep your visualization in its current state. diff --git a/docs/visualize/saving.asciidoc b/docs/visualize/saving.asciidoc index 855555794a6f0..e3330446bfad1 100644 --- a/docs/visualize/saving.asciidoc +++ b/docs/visualize/saving.asciidoc @@ -1,24 +1,19 @@ [[save-visualize]] -== Saving Visualizations -Saving visualizations enables you to reload them in Visualize and use them in -<>. +== Save visualizations +To use your visualizations in <>, you must save them. -[float] -[[visualize-read-only-access]] -=== [xpack]#Read only access# -When you have insufficient privileges to save visualizations, the following indicator in Kibana will be -displayed and the *Save* button won't be visible. For more information on granting access to -Kibana see <>. +. In the {kib} toolbar, click *Save*. +. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] +To access the saved visualization, go to *Management > {kib} > Saved Objects*. [float] -[[saving-a-visualization]] -=== Saving a Visualization -To save the current visualization: +[[save-visualization-read-only-access]] +==== Read only access +When you have insufficient privileges to save visualizations, the following indicator is +displayed and the *Save* button is not visible. -. Click *Save* in the Kibana toolbar. -. Enter a name for the visualization and click *Save*. +[role="screenshot"] +image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] -You can import, export and delete saved visualizations from *Management/Kibana/Saved Objects*. +For more information, see <>. diff --git a/docs/visualize/tagcloud.asciidoc b/docs/visualize/tagcloud.asciidoc deleted file mode 100644 index 04aef6af9df7c..0000000000000 --- a/docs/visualize/tagcloud.asciidoc +++ /dev/null @@ -1,41 +0,0 @@ -[[tagcloud-chart]] -== Tag Clouds - -A tag cloud visualization is a visual representation of text data, typically used to visualize free form text. -Tags are usually single words, and the importance of each tag is shown with font size or color. - -The font size for each word is determined by the _metrics_ aggregation. The following aggregations are available for -this chart: - -include::y-axis-aggs.asciidoc[] - - -The _buckets_ aggregations determine what information is being retrieved from your data set. - -Before you choose a buckets aggregation, select the *Split Tags* option. - -You can specify the following bucket aggregations for tag cloud visualization: - -*Terms*:: A {ref}/search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top -or bottom _n_ elements of a given field to display, ordered by count or a custom metric. - -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - - -Select the *Options* tab to change the following aspects of the chart: - -*Text Scale*:: You can select *linear*, *log*, or *square root* scales for the text scale. You can use a log -scale to display data that varies exponentially or a square root scale to -regularize the display of data sets with variabilities that are themselves highly variable. -*Orientation*:: You can select how to orientate your text in the tag cloud. You can choose one of the following options: -Single, right angles and multiple. -*Font Size*:: Allows you to set minimum and maximum font size to use for this visualization. diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 0e89704b8ba0b..a4d995982bdc7 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -44,7 +44,7 @@ Enter a string in the *Custom Label* field to change the display label. Coordinate maps use the {ref}/search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] aggregation. Select a field, typically coordinates, from the drop-down. -- The_Change precision on map zoom_ box is checked by default. Uncheck the box to disable this behavior. +- The _Change precision on map zoom_ box is checked by default. Uncheck the box to disable this behavior. The _Precision_ slider determines the granularity of the results displayed on the map. See the documentation for the {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid] aggregation for details on the area specified by each precision level. @@ -59,25 +59,9 @@ of the geohash grid cell. Leaving this checked generally results in a more accur Enter a string in the *Custom Label* field to change the display label. -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*Exclude Pattern*:: Specify a pattern in this field to exclude from the results. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - [float] ==== Options - *Map type*:: Select one of the following options from the drop-down. *_Scaled Circle Markers_*:: Scale the size of the markers based on the metric aggregation's value. *_Shaded Circle Markers_*:: Displays the markers with different shades based on the metric aggregation's value. diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc index c2707e2d67102..110533589cab9 100644 --- a/docs/visualize/visualize_rollup_data.asciidoc +++ b/docs/visualize/visualize_rollup_data.asciidoc @@ -1,33 +1,33 @@ [role="xpack"] [[visualize-rollup-data]] -== Using rolled up data in a visualization +== Use rolled up data in a visualization beta[] -You can visualize your rolled up data in a variety of charts, tables, maps, and -more. Most visualizations support rolled up data, with the exception of -Timelion, TSVB, and Vega visualizations. +You can visualize your rolled up data in a variety of charts, tables, maps, and +more. Most visualizations support rolled up data, with the exception of +Timelion, TSVB, and Vega visualizations. -To get started, go to *Management > Kibana > Index patterns.* -If a rollup index is detected in the cluster, *Create index pattern* -includes an item for creating a rollup index pattern. +To get started, go to *Management > Kibana > Index patterns.* +If a rollup index is detected in the cluster, *Create index pattern* +includes an item for creating a rollup index pattern. [role="screenshot"] image::images/management_create_rollup_menu.png[Create index pattern menu] -You can match an index pattern to only rolled up data, or mix both rolled up -and raw data to visualize all data together. An index pattern can match only one -rolled up index, not multiple. There is no restriction on the number of standard -indices that an index pattern can match. When matching multiple indices, -use a comma to separate the names, with no space after the comma. +You can match an index pattern to only rolled up data, or mix both rolled up +and raw data to visualize all data together. An index pattern can match only one +rolled up index, not multiple. There is no restriction on the number of standard +indices that an index pattern can match. When matching multiple indices, +use a comma to separate the names, with no space after the comma. Keep the following in mind when creating a visualization from rolled up data: -* The data in a rollup index only has summarized metrics for specific fields. -You can’t search any other field from the original raw data. -* Data is summarized into time buckets that might be split into sub buckets for -numeric field values or terms. You can ask for a time aggregation that takes -several time buckets and combines them to lower granularity. For example, +* The data in a rollup index only has summarized metrics for specific fields. +You can’t search any other field from the original raw data. +* Data is summarized into time buckets that might be split into sub buckets for +numeric field values or terms. You can ask for a time aggregation that takes +several time buckets and combines them to lower granularity. For example, if the rollup job was aggregated by hours, you can ask for buckets of days. The following visualization of rolled up data shows the date histogram @@ -36,9 +36,8 @@ interval multiple and the limited metrics aggregations. [role="screenshot"] image::images/management_rollups_visualization.png[][Rollups in visualizations] -Dashboards can have a mixture of rollup visualizations and regular visualizations, +Dashboards can have a mixture of rollup visualizations and regular visualizations, as shown in the following figure. Note that not all queries and filters support rollups. [role="screenshot"] image::images/management_rolled_dashboard.png[][Rollups in dashboards] - diff --git a/docs/visualize/x-axis-aggs.asciidoc b/docs/visualize/x-axis-aggs.asciidoc deleted file mode 100644 index 7d55ed1a98e7f..0000000000000 --- a/docs/visualize/x-axis-aggs.asciidoc +++ /dev/null @@ -1,44 +0,0 @@ -The X axis of this chart is the _buckets_ axis. You can define buckets for the X axis, for a split area on the -chart, or for split charts. - -This chart's X axis supports the following aggregations. Click the linked name of each aggregation to visit the main -Elasticsearch documentation for that aggregation. - -*Date Histogram*:: A {ref}/search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a -numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days, -weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, -down to one second. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch. -For example, the tooltip for a monthly interval will show the first day of the month. - -*Histogram*:: A standard {ref}/search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a -numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty -intervals in the histogram. -*Range*:: With a {ref}/search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges -of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove -a range. -*Date Range*:: A {ref}/search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values -that are within a range of dates that you specify. You can specify the ranges for the dates using -{ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. -Click the red *(x)* symbol to remove a range. -*IPv4 Range*:: The {ref}/search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to -specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to -remove a range. -*Terms*:: A {ref}/search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top -or bottom _n_ elements of a given field to display, ordered by count or a custom metric. -*Filters*:: You can specify a set of {ref}/search-aggregations-bucket-filters-aggregation.html[_filters_] for the data. -You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where -you can type in a name to display on the visualization. -*Significant Terms*:: Displays the results of the experimental -{ref}/search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation. - -Once you've specified an X axis aggregation, you can define sub-aggregations to refine the visualization. Click *+ Add -Sub Aggregation* to define a sub-aggregation, then choose *Split Area* or *Split Chart*, then select a sub-aggregation -from the list of types. - -When multiple aggregations are defined on a chart's axis, you can use the up or down arrows to the right of the -aggregation's type to change the aggregation's priority. - -Enter a string in the *Custom Label* field to change the display label. diff --git a/docs/visualize/xychart.asciidoc b/docs/visualize/xychart.asciidoc deleted file mode 100644 index 816efdef5b0b4..0000000000000 --- a/docs/visualize/xychart.asciidoc +++ /dev/null @@ -1,99 +0,0 @@ -[[xy-chart]] -== Line, Area, and Bar charts -Line, Area, and Bar charts allow you to plot your data on X/Y axis. - -First you need to select your _metrics_ which define Value axis. - -include::y-axis-aggs.asciidoc[] - -The _buckets_ aggregations determine what information is being retrieved from your data set. - -Before you choose a buckets aggregation, specify if you are splitting slices within a single chart or splitting into -multiple charts. A multiple chart split must run before any other aggregations. When you split a chart, you can change -if the splits are displayed in a row or a column by clicking the *Rows | Columns* selector. - -include::x-axis-aggs.asciidoc[] - -include::color-picker.asciidoc[] - -Enter a string in the *Custom Label* field to change the display label. - -You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: - -*Exclude Pattern*:: Specify a pattern in this field to exclude from the results. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{ref}/modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -[float] -[[metrics-axes]] -=== Metrics & Axes - -Select the *Metrics & Axes* tab to change the way each individual metric is shown on the chart. -The data series are styled in the _Metrics_ section, while the axes are styled in the X and Y axis sections. - -[float] -==== Metrics -Modify how each metric from the Data panel is visualized on the chart. - -*Chart type*:: Choose between *Area*, *Line*, and *Bar* types. -*Mode*:: stack the different metrics, or plot them next to each other -*Value Axis*:: choose the axis you want to plot this data too (the properties of each are configured under Y-axes). -*Line mode*:: should the outline of lines or bars appear *smooth*, *straight*, or *stepped*. - -[float] -==== Y-axis - -Style all the Y-axes of the chart. - -*Position*:: position of the Y-axis (*left* or *right* for vertical charts, and *top* or *bottom* for horizontal charts). -*Scale type*:: scaling of the values (*linear*, *log*, or *square root*) -*Advanced Options*:: -*Labels - Show Labels*:::: Allows you to hide axis labels -*Labels - Filter Labels*:::: If filter labels is enabled some labels will be hidden in case there is not enough space to display them -*Labels - Rotate*:::: You can enter the number in degrees for how much you want to rotate labels -*Labels - Truncate*:::: You can enter the size in pixels to which the label is truncated -*Scale to Data Bounds*:::: The default Y-axis bounds are zero and the maximum value returned in the data. Check - this box to change both upper and lower bounds to match the values returned in the data. - Checking this option may cause that the bar, which value equals to the lower bounds/ - upper bounds (in case only negative values are depicted) is hidden. - To avoid that, you can define bounds margin. Via bounds margin you specify a value, - which decreases/increases the lower/upper bounds when displaying the plot. -*Custom Extents*:::: You can define custom minimum and maximum for each axis - -[float] -==== X-Axis - -*Position*:: position of the X-Axis (*left* or *right* for horizontal charts, and *top* or *bottom* for vertical charts). -*Advanced Options*:: -*Labels - Show Labels*:::: Allows you to hide axis labels -*Labels - Filter Labels*:::: If filter labels is enabled some labels will be hidden in case there is not enough spave to display them -*Labels - Rotate*:::: You can enter the number in degrees for how much you want to rotate labels -*Labels - Truncate*:::: You can enter the size in pixels to which the label is truncated - -[float] -[[panel-settings]] -=== Panel Settings - -These are options that apply to the entire chart and not just the individual data series. - -[float] -==== Common options -*Legend Position*:: Move your legend to the *left*, *right*, *top* or *bottom* -*Show Tooltip*:: Enables or disables the display of tooltip on hovering over chart objects -*Current Time Marker*:: Show a line indicating the current time - -[float] -==== Grid options -You can enable grid on the chart. By default grid is displayed on the category axis only. - -*X-axis*:: You can disable the display of grid lines on category axis -*Y-axis*:: You can choose on which (if any) of the value axes you want to display grid lines diff --git a/docs/visualize/y-axis-aggs.asciidoc b/docs/visualize/y-axis-aggs.asciidoc deleted file mode 100644 index 1b21b94b702f5..0000000000000 --- a/docs/visualize/y-axis-aggs.asciidoc +++ /dev/null @@ -1,61 +0,0 @@ -Metric Aggregations: - -*Count*:: The {ref}/search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of -the elements in the selected index pattern. -*Average*:: This aggregation returns the {ref}/search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric -field. Select a field from the drop-down. -*Sum*:: The {ref}/search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric -field. Select a field from the drop-down. -*Min*:: The {ref}/search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a -numeric field. Select a field from the drop-down. -*Max*:: The {ref}/search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a -numeric field. Select a field from the drop-down. -*Unique Count*:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns -the number of unique values in a field. Select a field from the drop-down. -*Standard Deviation*:: The {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] -aggregation returns the standard deviation of data in a numeric field. Select a field from the drop-down. -*Top Hit*:: The {ref}/search-aggregations-metrics-top-hits-aggregation.html[_top hits_] -aggregation returns one or more of the top values from a specific field in your documents. Select a field from the drop-down, -how you want to sort the documents and choose the top fields, and how many values should be returned. -*Percentiles*:: The {ref}/search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a -percentile field. -*Percentile Rank*:: The {ref}/search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field -from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a -values field. Click *+Add* to add a values field. - -Parent Pipeline Aggregations: - -For each of the parent pipeline aggregations you have to define the metric for which the aggregation is calculated. -That could be one of your existing metrics or a new one. You can also nest this aggregations -(for example to produce 3rd derivative) - -*Derivative*:: The {ref}/search-aggregations-pipeline-derivative-aggregation.html[_derivative_] aggregation calculates -the derivative of specific metrics. -*Cumulative Sum*:: The {ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[_cumulative sum_] aggregation -calculates the cumulative sum of a specified metric in a parent histogram -*Moving Average*:: The {ref}/search-aggregations-pipeline-movavg-aggregation.html[_moving average_] aggregation will -slide a window across the data and emit the average value of that window -*Serial Diff*:: The {ref}/search-aggregations-pipeline-serialdiff-aggregation.html[_serial differencing_] is a technique -where values in a time series are subtracted from itself at different time lags or period - -Sibling Pipeline Aggregations: - -Just like with parent pipeline aggregations you need to provide a metric for which to calculate the sibling aggregation. -On top of that you also need to provide a bucket aggregation which will define the buckets on which the sibling -aggregation will run - -*Average Bucket*:: The {ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[_avg bucket_] -calculates the (mean) average value of a specified metric in a sibling aggregation -*Sum Bucket*:: The {ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[_sum bucket_] -calculates the sum of values of a specified metric in a sibling aggregation -*Min Bucket*:: The {ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[_min bucket_] -calculates the minimum value of a specified metric in a sibling aggregation -*Max Bucket*:: The {ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[_max bucket_] -calculates the maximum value of a specified metric in a sibling aggregation - -You can add an aggregation by clicking the *+ Add Metrics* button. - -Enter a string in the *Custom Label* field to change the display label. diff --git a/package.json b/package.json index ac06753d7d046..45a376a291359 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,11 @@ "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", - "**/image-diff/gm/debug": "^2.6.9" + "**/image-diff/gm/debug": "^2.6.9", + "**/deepmerge": "^4.2.2", + "**/react": "16.8.6", + "**/react-dom": "16.8.6", + "**/react-test-renderer": "16.8.6" }, "workspaces": { "packages": [ @@ -105,11 +109,11 @@ }, "dependencies": { "@babel/core": "^7.5.5", - "@babel/register": "^7.5.5", + "@babel/register": "^7.7.0", "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "14.9.0", + "@elastic/eui": "16.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -120,7 +124,6 @@ "@kbn/babel-code-parser": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", @@ -132,7 +135,7 @@ "@types/recompose": "^0.30.5", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.3.0", - "angular": "^1.7.8", + "angular": "^1.7.9", "angular-aria": "^1.7.8", "angular-elastic": "^2.5.1", "angular-recursion": "^1.0.5", @@ -155,6 +158,7 @@ "custom-event-polyfill": "^0.3.0", "d3": "3.5.17", "d3-cloud": "1.2.5", + "deepmerge": "^4.2.2", "del": "^5.1.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", @@ -169,7 +173,7 @@ "globby": "^8.0.1", "good-squeeze": "2.1.0", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", @@ -212,10 +216,10 @@ "pug": "^2.0.3", "querystring-browser": "1.0.4", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-addons-shallow-compare": "15.6.2", "react-color": "^2.13.8", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-grid-layout": "^0.16.2", "react-hooks-testing-library": "^0.5.0", "react-input-range": "^1.3.0", @@ -340,15 +344,15 @@ "@types/semver": "^5.5.0", "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", - "@types/styled-components": "^3.0.2", + "@types/styled-components": "^4.4.0", "@types/supertest": "^2.0.5", "@types/supertest-as-promised": "^2.0.38", "@types/type-detect": "^4.0.1", "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", @@ -366,9 +370,9 @@ "dedent": "^0.7.0", "delete-empty": "^2.0.0", "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", - "enzyme-adapter-utils": "^1.12.0", - "enzyme-to-json": "^3.3.4", + "enzyme-adapter-react-16": "^1.15.1", + "enzyme-adapter-utils": "^1.12.1", + "enzyme-to-json": "^3.4.3", "eslint": "^6.5.1", "eslint-config-prettier": "^6.4.0", "eslint-plugin-babel": "^5.3.0", @@ -421,7 +425,7 @@ "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", - "mocha": "6.2.1", + "mocha": "^6.2.2", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 71517bc10404d..ee65a1cf79148 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 757616f36180b..8ffae5edc88eb 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/eslint-config-kibana/typescript.js @@ -70,7 +70,26 @@ module.exports = { // Old recommended tslint rules '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/array-type': ['error', { default: 'array-simple', readonly: 'array-simple' }], - '@typescript-eslint/ban-types': 'error', + '@typescript-eslint/ban-types': ['error', { + types: { + SFC: { + message: 'Use FC or FunctionComponent instead.', + fixWith: 'FC' + }, + 'React.SFC': { + message: 'Use FC or FunctionComponent instead.', + fixWith: 'React.FC' + }, + StatelessComponent: { + message: 'Use FunctionComponent instead.', + fixWith: 'FunctionComponent' + }, + 'React.StatelessComponent': { + message: 'Use FunctionComponent instead.', + fixWith: 'React.FunctionComponent' + } + } + }], 'camelcase': 'off', '@typescript-eslint/camelcase': ['error', { 'properties': 'never', diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md new file mode 100644 index 0000000000000..8ba2c43b5e1fe --- /dev/null +++ b/packages/kbn-config-schema/README.md @@ -0,0 +1,511 @@ +# `@kbn/config-schema` — The Kibana config validation library + +`@kbn/config-schema` is a TypeScript library inspired by Joi and designed to allow run-time validation of the +Kibana configuration entries providing developers with a fully typed model of the validated data. + +## Table of Contents + +- [Why `@kbn/config-schema`?](#why-kbnconfig-schema) +- [Schema building blocks](#schema-building-blocks) + - [Basic types](#basic-types) + - [`schema.string()`](#schemastring) + - [`schema.number()`](#schemanumber) + - [`schema.boolean()`](#schemaboolean) + - [`schema.literal()`](#schemaliteral) + - [Composite types](#composite-types) + - [`schema.arrayOf()`](#schemaarrayof) + - [`schema.object()`](#schemaobject) + - [`schema.recordOf()`](#schemarecordof) + - [`schema.mapOf()`](#schemamapof) + - [Advanced types](#advanced-types) + - [`schema.oneOf()`](#schemaoneof) + - [`schema.any()`](#schemaany) + - [`schema.maybe()`](#schemamaybe) + - [`schema.nullable()`](#schemanullable) + - [`schema.never()`](#schemanever) + - [`schema.uri()`](#schemauri) + - [`schema.byteSize()`](#schemabytesize) + - [`schema.duration()`](#schemaduration) + - [`schema.conditional()`](#schemaconditional) + - [References](#references) + - [`schema.contextRef()`](#schemacontextref) + - [`schema.siblingRef()`](#schemasiblingref) +- [Custom validation](#custom-validation) +- [Default values](#default-values) + +## Why `@kbn/config-schema`? + +Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates. + +There are a number of reasons why we decided to roll our own solution for the configuration validation: + +* **Limited API surface** - having a future rich library is awesome, but it's a really hard task to audit such library and make sure everything is sane and secure enough. As everyone knows complexity is the enemy of security and hence we'd like to have a full control over what exactly we expose and commit to maintain. +* **Custom error messages** - detailed validation error messages are a great help to developers, but at the same time they can contain information that's way too sensitive to expose to everyone. We'd like to control these messages and make them only as detailed as really needed. For example, we don't want validation error messages to contain the passwords for internal users to show-up in the logs. These logs are commonly ingested into Elasticsearch, and accessible to a large number of users which shouldn't have access to the internal user's password. +* **Type information** - having run-time guarantees is great, but additionally having compile-time guarantees is even better. We'd like to provide developers with a fully typed model of the validated data so that it's harder to misuse it _after_ validation. +* **Upgradability** - no matter how well a validation library is implemented, it will have bugs and may need to be improved at some point anyway. Some external libraries are very well supported, some aren't or won't be in the future. It's always a risk to depend on an external party with their own release cadence when you need to quickly fix a security vulnerability in a patch version. We'd like to have a better control over lifecycle of such an important piece of our codebase. + +## Schema building blocks + +The schema is composed of one or more primitives depending on the shape of the data you'd like to validate. + +```typescript +const simpleStringSchema = schema.string(); +const moreComplexObjectSchema = schema.object({ name: schema.string() }); +``` + +Every schema instance has a `validate` method that is used to perform a validation of the data according to the schema. This method accepts three arguments: + +* `data: any` - **required**, data to be validated with the schema +* `context: Record` - **optional**, object whose properties can be referenced by the [context references](#schemacontextref) +* `namespace: string` - **optional**, arbitrary string that is used to prefix every error message thrown during validation + +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean(), + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); + +expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({ + isEnabled: true, + env: 'prod', +}); + +// Use default value for `env` from context via reference +expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({ + isEnabled: true, + env: 'staging', +}); + +// Fail because of type mismatch +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }) +).toThrowError( + '[isEnabled]: expected value of type [boolean] but got [string]' +); + +// Fail because of type mismatch and prefix error with a custom namespace +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration') +).toThrowError( + '[configuration.isEnabled]: expected value of type [boolean] but got [string]' +); +``` + +__Notes:__ +* `validate` method throws as soon as the first schema violation is encountered, no further validation is performed. +* when you retrieve configuration within a Kibana plugin `validate` function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.). + +### Basic types + +#### `schema.string()` + +Validates input data as a string. + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minLength: number` - defines a minimum length the string should have. + * `maxLength: number` - defines a maximum length the string should have. + * `hostname: boolean` - indicates whether the string should be validated as a valid hostname (per [RFC 1123](https://tools.ietf.org/html/rfc1123)). + +__Usage:__ +```typescript +const valueSchema = schema.string({ maxLength: 10 }); +``` + +__Notes:__ +* By default `schema.string()` allows empty strings, to prevent that use non-zero value for `minLength` option. +* To validate a string using a regular expression use a custom validator function, see [Custom validation](#custom-validation) section for more details. + +#### `schema.number()` + +Validates input data as a number. + +__Output type:__ `number` + +__Options:__ + * `defaultValue: number | Reference | (() => number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: number` - defines a minimum value the number should have. + * `max: number` - defines a maximum value the number should have. + +__Usage:__ +```typescript +const valueSchema = schema.number({ max: 10 }); +``` + +__Notes:__ +* The `schema.number()` also supports a string as input if it can be safely coerced into number. + +#### `schema.boolean()` + +Validates input data as a boolean. + +__Output type:__ `boolean` + +__Options:__ + * `defaultValue: boolean | Reference | (() => boolean)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: boolean) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.boolean({ defaultValue: false }); +``` + +#### `schema.literal()` + +Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. + +__Output type:__ `string`, `number` or `boolean` literals + +__Options:__ + * `defaultValue: TLiteral | Reference | (() => TLiteral)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TLiteral) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = [ + schema.literal('stringLiteral'), + schema.literal(100500), + schema.literal(false), +]; +``` + +### Composite types + +#### `schema.arrayOf()` + +Validates input data as a homogeneous array with the values being validated against predefined schema. + +__Output type:__ `TValue[]` + +__Options:__ + * `defaultValue: TValue[] | Reference | (() => TValue[])` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minSize: number` - defines a minimum size the array should have. + * `maxSize: number` - defines a maximum size the array should have. + +__Usage:__ +```typescript +const valueSchema = schema.arrayOf(schema.number()); +``` + +#### `schema.object()` + +Validates input data as an object with a predefined set of properties. + +__Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` + +__Options:__ + * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean({ defaultValue: false }), + name: schema.string({ minLength: 10 }), +}); +``` + +__Notes:__ +* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. + +#### `schema.recordOf()` + +Validates input data as an object with the keys and values being validated against predefined schema. + +__Output type:__ `Record` + +__Options:__ + * `defaultValue: Record | Reference> | (() => Record)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Record) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.recordOf(schema.string(), schema.number()); +``` + +__Notes:__ +* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. + +#### `schema.mapOf()` + +Validates input data as a map with the keys and values being validated against the predefined schema. + +__Output type:__ `Map` + +__Options:__ + * `defaultValue: Map | Reference> | (() => Map)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Map) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.mapOf(schema.string(), schema.number()); +``` + +### Advanced types + +#### `schema.oneOf()` + +Allows a list of alternative schemas to validate input data against. + +__Output type:__ `TValue1 | TValue2 | TValue3 | ..... as TUnion` + +__Options:__ + * `defaultValue: TUnion | Reference | (() => TUnion)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]); +``` + +__Notes:__ +* Since the result data type is a type union you should use various TypeScript type guards to get the exact type. + +#### `schema.any()` + +Indicates that input data shouldn't be validated and returned as is. + +__Output type:__ `any` + +__Options:__ + * `defaultValue: any | Reference | (() => any)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: any) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.any(); +``` + +__Notes:__ +* `schema.any()` is essentially an escape hatch for the case when your data can __really__ have any type and should be avoided at all costs. + +#### `schema.maybe()` + +Indicates that input data is optional and may not be present. + +__Output type:__ `T | undefined` + +__Usage:__ +```typescript +const valueSchema = schema.maybe(schema.string()); +``` + +__Notes:__ +* Don't use `schema.maybe()` if a nested type defines a default value. + +#### `schema.nullable()` + +Indicates that input data is optional and defaults to `null` if it's not present. + +__Output type:__ `T | null` + +__Usage:__ +```typescript +const valueSchema = schema.nullable(schema.string()); +``` + +__Notes:__ +* `schema.nullable()` also treats explicitly specified `null` as a valid input. + +#### `schema.never()` + +Indicates that input data is forbidden. + +__Output type:__ `never` + +__Usage:__ +```typescript +const valueSchema = schema.never(); +``` + +__Notes:__ +* `schema.never()` has a very limited application and usually used within [conditional schemas](#schemaconditional) to fully or partially forbid input data. + +#### `schema.uri()` + +Validates input data as a proper URI string (per [RFC 3986](https://tools.ietf.org/html/rfc3986)). + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `scheme: string | string[]` - limits allowed URI schemes to the one(s) defined here. + +__Usage:__ +```typescript +const valueSchema = schema.uri({ scheme: 'https' }); +``` + +__Notes:__ +* Prefer using `schema.uri()` for all URI validations even though it may be possible to replicate it with a custom validator for `schema.string()`. + +#### `schema.byteSize()` + +Validates input data as a proper digital data size. + +__Output type:__ `ByteSizeValue` + +__Options:__ + * `defaultValue: ByteSizeValue | string | number | Reference | (() => ByteSizeValue | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: ByteSizeValue | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: ByteSizeValue | string | number` - defines a minimum value the size should have. + * `max: ByteSizeValue | string | number` - defines a maximum value the size should have. + +__Usage:__ +```typescript +const valueSchema = schema.byteSize({ min: '3kb' }); +``` + +__Notes:__ +* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`. +* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. +* Currently you cannot specify zero bytes with a string format and should use number `0` instead. + +#### `schema.duration()` + +Validates input data as a proper [duration](https://momentjs.com/docs/#/durations/). + +__Output type:__ `moment.Duration` + +__Options:__ + * `defaultValue: moment.Duration | string | number | Reference | (() => moment.Duration | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: moment.Duration | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.duration({ defaultValue: '70ms' }); +``` + +__Notes:__ +* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. +* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. + +#### `schema.conditional()` + +Allows a specified condition that is evaluated _at the validation time_ and results in either one or another input validation schema. + +The first argument is always a [reference](#references) while the second one can be: +* another reference, in this cases both references are "dereferenced" and compared +* schema, in this case the schema is used to validate "dereferenced" value of the first reference +* value, in this case "dereferenced" value of the first reference is compared to that value + +The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to `true`, otherwise `schema.conditional()` should fallback +to the schema provided as the fourth argument. + +__Output type:__ `TTrueResult | TFalseResult` + +__Options:__ + * `defaultValue: TTrueResult | TFalseResult | Reference | (() => TTrueResult | TFalseResult` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TTrueResult | TFalseResult) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + key: schema.oneOf([schema.literal('number'), schema.literal('string')]), + value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), +}); +``` + +__Notes:__ +* Conditional schemas may be hard to read and understand and hence should be used only sparingly. + +### References + +#### `schema.contextRef()` + +Defines a reference to the value specified through the validation context. Context reference is only used as part of a [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); +valueSchema.validate({}, { envName: 'dev' }); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. +* The root context that Kibana provides during config validation includes lots of useful properties like `environment name` that can be used to provide a strict schema for production and more relaxed one for development. + +#### `schema.siblingRef()` + +Defines a reference to the value of the sibling key. Sibling references are only used a part of [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + node: schema.object({ tag: schema.string() }), + env: schema.string({ defaultValue: schema.siblingRef('node.tag') }), +}); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. + +## Custom validation + +Using built-in schema primitives may not be enough in some scenarios or sometimes the attempt to model complex schemas with built-in primitives only may result in unreadable code. +For these cases `@kbn/config-schema` provides a way to specify a custom validation function for almost any schema building block through the `validate` option. + +For example `@kbn/config-schema` doesn't have a dedicated primitive for the `RegExp` based validation currently, but you can easily do that with a custom `validate` function: + +```typescript +const valueSchema = schema.string({ + minLength: 3, + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, +}); + +// ...or if you use that construct a lot... + +const regexSchema = (regex: RegExp) => schema.string({ + validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`, +}); +const valueSchema = regexSchema(/^[a-z0-9_-]+$/); +``` + +Custom validation function is run _only after_ all built-in validations passed. It should either return a `string` as an error message +to denote the failed validation or not return anything at all (`void`) otherwise. Please also note that `validate` function is synchronous. + +Another use case for custom validation functions is when the schema depends on some run-time data: + +```typescript +const gesSchema = randomRunTimeSeed => schema.string({ + validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined +}); + +const schema = gesSchema('some-random-run-time-data'); +``` + +## Default values + +If you have an optional config field that you can have a default value for you may want to consider using dedicated `defaultValue` option to not +deal with "defined or undefined"-like checks all over the place in your code. You have three options to provide a default value for almost any schema primitive: + +* plain value that's known at the compile time +* [reference](#references) to a value that will be "dereferenced" at the validation time +* function that is invoked at the validation time and returns a plain value + +```typescript +const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' }); +const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') }); +const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() }); +``` + +__Notes:__ +* `@kbn/config-schema` neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type. diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md deleted file mode 100644 index fc403447877d8..0000000000000 --- a/packages/kbn-es-query/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# kbn-es-query - -This module is responsible for generating Elasticsearch queries for Kibana. See explanations below for each of the subdirectories. - -## es_query - -This folder contains the code that combines Lucene/KQL queries and filters into an Elasticsearch query. - -```javascript -buildEsQuery(indexPattern, queries, filters, config) -``` - -Generates the Elasticsearch query DSL from combining the queries and filters provided. - -```javascript -buildQueryFromFilters(filters, indexPattern) -``` - -Generates the Elasticsearch query DSL from the given filters. - -```javascript -luceneStringToDsl(query) -``` - -Generates the Elasticsearch query DSL from the given Lucene query. - -```javascript -migrateFilter(filter, indexPattern) -``` - -Migrates a filter from a previous version of Elasticsearch to the current version. - -```javascript -decorateQuery(query, queryStringOptions) -``` - -Decorates an Elasticsearch query_string query with the given options. - -## filters - -This folder contains the code related to Kibana Filter objects, including their definitions, and helper functions to create them. Filters in Kibana always contain a `meta` property which describes which `index` the filter corresponds to, as well as additional data about the specific filter. - -The object that is created by each of the following functions corresponds to a Filter object in the `lib` directory (e.g. `PhraseFilter`, `RangeFilter`, etc.) - -```javascript -buildExistsFilter(field, indexPattern) -``` - -Creates a filter (`ExistsFilter`) where the given field exists. - -```javascript -buildPhraseFilter(field, value, indexPattern) -``` - -Creates an filter (`PhraseFilter`) where the given field matches the given value. - -```javascript -buildPhrasesFilter(field, params, indexPattern) -``` - -Creates a filter (`PhrasesFilter`) where the given field matches one or more of the given values. `params` should be an array of values. - -```javascript -buildQueryFilter(query, index) -``` - -Creates a filter (`CustomFilter`) corresponding to a raw Elasticsearch query DSL object. - -```javascript -buildRangeFilter(field, params, indexPattern) -``` - -Creates a filter (`RangeFilter`) where the value for the given field is in the given range. `params` should contain `lt`, `lte`, `gt`, and/or `gte`. - -## kuery - -This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language. - -In general, you will only need to worry about the following functions from the `ast` folder: - -```javascript -fromExpression(expression) -``` - -Generates an abstract syntax tree corresponding to the raw Kibana query `expression`. - -```javascript -toElasticsearchQuery(node, indexPattern) -``` - -Takes an abstract syntax tree (generated from the previous method) and generates the Elasticsearch query DSL using the given `indexPattern`. Note that if no `indexPattern` is provided, then an Elasticsearch query DSL will still be generated, ignoring things like the index pattern scripted fields, field types, etc. - diff --git a/packages/kbn-es-query/babel.config.js b/packages/kbn-es-query/babel.config.js deleted file mode 100644 index 68783433fc711..0000000000000 --- a/packages/kbn-es-query/babel.config.js +++ /dev/null @@ -1,35 +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. - */ - -// We can't use common Kibana presets here because of babel versions incompatibility -module.exports = { - env: { - public: { - presets: [ - '@kbn/babel-preset/webpack_preset' - ], - }, - server: { - presets: [ - '@kbn/babel-preset/node_preset' - ], - }, - }, - ignore: ['**/__tests__/**/*', '**/*.test.ts', '**/*.test.tsx'], -}; diff --git a/packages/kbn-es-query/index.d.ts b/packages/kbn-es-query/index.d.ts deleted file mode 100644 index 9bbd0a193dfed..0000000000000 --- a/packages/kbn-es-query/index.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './src'; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json deleted file mode 100644 index 2cd2a8f53d2ee..0000000000000 --- a/packages/kbn-es-query/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@kbn/es-query", - "main": "target/server/index.js", - "browser": "target/public/index.js", - "version": "1.0.0", - "license": "Apache-2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --source-maps --watch" - }, - "dependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", - "moment-timezone": "^0.5.27", - "@kbn/i18n": "1.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", - "@kbn/expect": "1.0.0", - "del": "^5.1.0", - "getopts": "^2.2.4", - "supports-color": "^7.0.0" - } -} diff --git a/packages/kbn-es-query/scripts/build.js b/packages/kbn-es-query/scripts/build.js deleted file mode 100644 index 6d53a8469b0e0..0000000000000 --- a/packages/kbn-es-query/scripts/build.js +++ /dev/null @@ -1,20 +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. - */ - -require('../tasks/build_cli'); diff --git a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json b/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json deleted file mode 100644 index 1799d04a0fbd8..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "meta": { - "index": "logstash-*" - } -} diff --git a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json b/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json deleted file mode 100644 index 588e6ada69cfe..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "id": "logstash-*", - "title": "logstash-*", - "fields": [ - { - "name": "bytes", - "type": "number", - "esTypes": ["long"], - "count": 10, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ssl", - "type": "boolean", - "esTypes": ["boolean"], - "count": 20, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@timestamp", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "time", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@tags", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "utc_time", - "type": "date", - "esTypes": ["date"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "phpmemory", - "type": "number", - "esTypes": ["integer"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ip", - "type": "ip", - "esTypes": ["ip"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "request_body", - "type": "attachment", - "esTypes": ["attachment"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "point", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "area", - "type": "geo_shape", - "esTypes": ["geo_shape"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "hashed", - "type": "murmur3", - "esTypes": ["murmur3"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "geo.coordinates", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "extension", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "machine.os", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "machine.os.raw", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true, - "subType": { "multi": { "parent": "machine.os" } } - }, - { - "name": "geo.src", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "_id", - "type": "string", - "esTypes": ["_id"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_type", - "type": "string", - "esTypes": ["_type"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_source", - "type": "_source", - "esTypes": ["_source"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-filterable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-sortable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "custom_user_field", - "type": "conflict", - "esTypes": ["long", "text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "script string", - "type": "string", - "count": 0, - "scripted": true, - "script": "'i am a string'", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script number", - "type": "number", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script date", - "type": "date", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "painless", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script murmur3", - "type": "murmur3", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "nestedField.child", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField" } } - }, - { - "name": "nestedField.nestedChild.doublyNestedChild", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField.nestedChild" } } - } - ] -} diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/index.js b/packages/kbn-es-query/src/index.js deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.js +++ /dev/null @@ -1,20 +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. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js deleted file mode 100644 index 3cbe1203bc533..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js +++ /dev/null @@ -1,415 +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 * as ast from '../ast'; -import expect from '@kbn/expect'; -import { nodeTypes } from '../../node_types/index'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery AST API', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('fromKueryExpression', function () { - - it('should return a match all "is" function for whitespace', function () { - const expected = nodeTypes.function.buildNode('is', '*', '*'); - const actual = ast.fromKueryExpression(' '); - expect(actual).to.eql(expected); - }); - - it('should return an "is" function with a null field for single literals', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression('foo'); - expect(actual).to.eql(expected); - }); - - it('should ignore extraneous whitespace at the beginning and end of the query', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression(' foo '); - expect(actual).to.eql(expected); - }); - - it('should not split on whitespace', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); - const actual = ast.fromKueryExpression('foo bar'); - expect(actual).to.eql(expected); - }); - - it('should support "and" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo and bar'); - expect(actual).to.eql(expected); - }); - - it('should support "or" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo or bar'); - expect(actual).to.eql(expected); - }); - - it('should support negation of queries with a "not" prefix', function () { - const expected = nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]) - ); - const actual = ast.fromKueryExpression('not (foo or bar)'); - expect(actual).to.eql(expected); - }); - - it('"and" should have a higher precedence than "or"', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'bar'), - nodeTypes.function.buildNode('is', null, 'baz'), - ]), - nodeTypes.function.buildNode('is', null, 'qux'), - ]) - ]); - const actual = ast.fromKueryExpression('foo or bar and baz or qux'); - expect(actual).to.eql(expected); - }); - - it('should support grouping to override default precedence', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]), - nodeTypes.function.buildNode('is', null, 'baz'), - ]); - const actual = ast.fromKueryExpression('(foo or bar) and baz'); - expect(actual).to.eql(expected); - }); - - it('should support matching against specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); - const actual = ast.fromKueryExpression('foo:bar'); - expect(actual).to.eql(expected); - }); - - it('should also not split on whitespace when matching specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); - const actual = ast.fromKueryExpression('foo:bar baz'); - expect(actual).to.eql(expected); - }); - - it('should treat quoted values as phrases', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); - const actual = ast.fromKueryExpression('foo:"bar baz"'); - expect(actual).to.eql(expected); - }); - - it('should support a shorthand for matching multiple values against a single field', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]); - const actual = ast.fromKueryExpression('foo:(bar or baz)'); - expect(actual).to.eql(expected); - }); - - it('should support "and" and "not" operators and grouping in the shorthand as well', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]), - nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('is', 'foo', 'qux') - ), - ]); - const actual = ast.fromKueryExpression('foo:((bar or baz) and not qux)'); - expect(actual).to.eql(expected); - }); - - it('should support exclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gt: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lt: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes > 1000 and bytes < 8000'); - expect(actual).to.eql(expected); - }); - - it('should support inclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gte: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lte: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes >= 1000 and bytes <= 8000'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in field names', function () { - const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); - const actual = ast.fromKueryExpression('machine*:osx'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in values', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); - const actual = ast.fromKueryExpression('foo:ba*'); - expect(actual).to.eql(expected); - }); - - it('should create an exists "is" query when a field is given and "*" is the value', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', '*'); - const actual = ast.fromKueryExpression('foo:*'); - expect(actual).to.eql(expected); - }); - - it('should support nested queries indicated by curly braces', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ); - const actual = ast.fromKueryExpression('nestedField:{ childOfNested: foo }'); - expect(actual).to.eql(expected); - }); - - it('should support nested subqueries and subqueries inside nested queries', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), - nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), - ]) - )]); - const actual = ast.fromKueryExpression('response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }'); - expect(actual).to.eql(expected); - }); - - it('should support nested sub-queries inside paren groups', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'bar') - ), - ]) - ]); - const actual = ast.fromKueryExpression('response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )'); - expect(actual).to.eql(expected); - }); - - it('should support nested groups inside other nested groups', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode( - 'nested', - 'nestedChild', - nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') - ) - ); - const actual = ast.fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); - expect(actual).to.eql(expected); - }); - }); - - describe('fromLiteralExpression', function () { - - it('should create literal nodes for unquoted values with correct primitive types', function () { - const stringLiteral = nodeTypes.literal.buildNode('foo'); - const booleanFalseLiteral = nodeTypes.literal.buildNode(false); - const booleanTrueLiteral = nodeTypes.literal.buildNode(true); - const numberLiteral = nodeTypes.literal.buildNode(42); - - expect(ast.fromLiteralExpression('foo')).to.eql(stringLiteral); - expect(ast.fromLiteralExpression('true')).to.eql(booleanTrueLiteral); - expect(ast.fromLiteralExpression('false')).to.eql(booleanFalseLiteral); - expect(ast.fromLiteralExpression('42')).to.eql(numberLiteral); - }); - - it('should allow escaping of special characters with a backslash', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - // yo dawg - const actual = ast.fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); - expect(actual).to.eql(expected); - }); - - it('should support double quoted strings that do not need escapes except for quotes', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - const actual = ast.fromLiteralExpression('"\\():<>\\"*"'); - expect(actual).to.eql(expected); - }); - - it('should support escaped backslashes inside quoted strings', function () { - const expected = nodeTypes.literal.buildNode('\\'); - const actual = ast.fromLiteralExpression('"\\\\"'); - expect(actual).to.eql(expected); - }); - - it('should detect wildcards and build wildcard AST nodes', function () { - const expected = nodeTypes.wildcard.buildNode('foo*bar'); - const actual = ast.fromLiteralExpression('foo*bar'); - expect(actual).to.eql(expected); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given node type\'s ES query representation', function () { - const node = nodeTypes.function.buildNode('exists', 'response'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); - const result = ast.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an empty "and" function for undefined nodes and unknown node types', function () { - const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); - - expect(ast.toElasticsearchQuery()).to.eql(expected); - - const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - delete noTypeNode.type; - expect(ast.toElasticsearchQuery(noTypeNode)).to.eql(expected); - - const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - unknownTypeNode.type = 'notValid'; - expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected); - }); - - it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); - const result = ast.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - }); - - describe('doesKueryExpressionHaveLuceneSyntaxError', function () { - it('should return true for Lucene ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); - expect(result).to.eql(true); - }); - - it('should return false for KQL ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); - expect(result).to.eql(true); - }); - - it('should return false for KQL exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar:*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); - expect(result).to.eql(true); - }); - - it('should return false for KQL wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene regex', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene fuzziness', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene proximity', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene boosting', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene + operator', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene - operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene && operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene || operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for mixed KQL/Lucene queries', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); - expect(result).to.eql(true); - }); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts deleted file mode 100644 index ef3d0ee828874..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ /dev/null @@ -1,50 +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 { JsonObject } from '..'; - -/** - * WARNING: these typings are incomplete - */ - -export type KueryNode = any; - -export type DslQuery = any; - -export interface KueryParseOptions { - helpers: { - [key: string]: any; - }; - startRule: string; - allowLeadingWildcards: boolean; -} - -export function fromKueryExpression( - expression: string | DslQuery, - parseOptions?: Partial -): KueryNode; - -export function toElasticsearchQuery( - node: KueryNode, - indexPattern?: any, - config?: Record, - context?: Record -): JsonObject; - -export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/packages/kbn-es-query/src/kuery/ast/ast.js deleted file mode 100644 index 1688995d46f80..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/ast.js +++ /dev/null @@ -1,81 +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 _ from 'lodash'; -import { nodeTypes } from '../node_types/index'; -import { parse as parseKuery } from './kuery'; -import { KQLSyntaxError } from '../errors'; - -export function fromLiteralExpression(expression, parseOptions) { - parseOptions = { - ...parseOptions, - startRule: 'Literal', - }; - - return fromExpression(expression, parseOptions, parseKuery); -} - -export function fromKueryExpression(expression, parseOptions) { - try { - return fromExpression(expression, parseOptions, parseKuery); - } catch (error) { - if (error.name === 'SyntaxError') { - throw new KQLSyntaxError(error, expression); - } else { - throw error; - } - } -} - -function fromExpression(expression, parseOptions = {}, parse = parseKuery) { - if (_.isUndefined(expression)) { - throw new Error('expression must be a string, got undefined instead'); - } - - parseOptions = { - ...parseOptions, - helpers: { nodeTypes }, - }; - - return parse(expression, parseOptions); -} - -/** - * @params {String} indexPattern - * @params {Object} config - contains the dateFormatTZ - * - * IndexPattern isn't required, but if you pass one in, we can be more intelligent - * about how we craft the queries (e.g. scripted fields) - */ -export function toElasticsearchQuery(node, indexPattern, config = {}, context = {}) { - if (!node || !node.type || !nodeTypes[node.type]) { - return toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); - } - - return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config, context); -} - -export function doesKueryExpressionHaveLuceneSyntaxError(expression) { - try { - fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); - return false; - } catch (e) { - return (e.message.startsWith('Lucene')); - } -} diff --git a/packages/kbn-es-query/src/kuery/ast/index.d.ts b/packages/kbn-es-query/src/kuery/ast/index.d.ts deleted file mode 100644 index 9e68d01d046cc..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/index.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from '../ast/ast'; diff --git a/packages/kbn-es-query/src/kuery/errors/index.js b/packages/kbn-es-query/src/kuery/errors/index.js deleted file mode 100644 index 82e1aee7b775a..0000000000000 --- a/packages/kbn-es-query/src/kuery/errors/index.js +++ /dev/null @@ -1,69 +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 { repeat } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', { - defaultMessage: 'end of input', -}); - -export class KQLSyntaxError extends Error { - - constructor(error, expression) { - const grammarRuleTranslations = { - fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', { - defaultMessage: 'field name', - }), - value: i18n.translate('kbnESQuery.kql.errors.valueText', { - defaultMessage: 'value', - }), - literal: i18n.translate('kbnESQuery.kql.errors.literalText', { - defaultMessage: 'literal', - }), - whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', { - defaultMessage: 'whitespace', - }), - }; - - const translatedExpectations = error.expected.map((expected) => { - return grammarRuleTranslations[expected.description] || expected.description; - }); - - const translatedExpectationText = translatedExpectations.join(', '); - - const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', { - defaultMessage: 'Expected {expectedList} but {foundInput} found.', - values: { - expectedList: translatedExpectationText, - foundInput: error.found ? `"${error.found}"` : endOfInputText, - }, - }); - - const fullMessage = [ - message, - expression, - repeat('-', error.location.start.offset) + '^', - ].join('\n'); - - super(fullMessage); - this.name = 'KQLSyntaxError'; - this.shortMessage = message; - } -} diff --git a/packages/kbn-es-query/src/kuery/errors/index.test.js b/packages/kbn-es-query/src/kuery/errors/index.test.js deleted file mode 100644 index d8040e464b696..0000000000000 --- a/packages/kbn-es-query/src/kuery/errors/index.test.js +++ /dev/null @@ -1,105 +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 { fromKueryExpression } from '../ast'; - - -describe('kql syntax errors', () => { - - it('should throw an error for a field query missing a value', () => { - expect(() => { - fromKueryExpression('response:'); - }).toThrow('Expected "(", "{", value, whitespace but end of input found.\n' + - 'response:\n' + - '---------^'); - }); - - it('should throw an error for an OR query missing a right side sub-query', () => { - expect(() => { - fromKueryExpression('response:200 or '); - }).toThrow('Expected "(", NOT, field name, value but end of input found.\n' + - 'response:200 or \n' + - '----------------^'); - }); - - it('should throw an error for an OR list of values missing a right side sub-query', () => { - expect(() => { - fromKueryExpression('response:(200 or )'); - }).toThrow('Expected "(", NOT, value but ")" found.\n' + - 'response:(200 or )\n' + - '-----------------^'); - }); - - it('should throw an error for a NOT query missing a sub-query', () => { - expect(() => { - fromKueryExpression('response:200 and not '); - }).toThrow('Expected "(", field name, value but end of input found.\n' + - 'response:200 and not \n' + - '---------------------^'); - }); - - it('should throw an error for a NOT list missing a sub-query', () => { - expect(() => { - fromKueryExpression('response:(200 and not )'); - }).toThrow('Expected "(", value but ")" found.\n' + - 'response:(200 and not )\n' + - '----------------------^'); - }); - - it('should throw an error for unbalanced quotes', () => { - expect(() => { - fromKueryExpression('foo:"ba '); - }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + - 'foo:"ba \n' + - '----^'); - }); - - it('should throw an error for unescaped quotes in a quoted string', () => { - expect(() => { - fromKueryExpression('foo:"ba "r"'); - }).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' + - 'foo:"ba "r"\n' + - '---------^'); - }); - - it('should throw an error for unescaped special characters in literals', () => { - expect(() => { - fromKueryExpression('foo:ba:r'); - }).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' + - 'foo:ba:r\n' + - '------^'); - }); - - it('should throw an error for range queries missing a value', () => { - expect(() => { - fromKueryExpression('foo > '); - }).toThrow('Expected literal, whitespace but end of input found.\n' + - 'foo > \n' + - '------^'); - }); - - it('should throw an error for range queries missing a field', () => { - expect(() => { - fromKueryExpression('< 1000'); - }).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + - '< 1000\n' + - '^'); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/packages/kbn-es-query/src/kuery/functions/__tests__/and.js deleted file mode 100644 index 07289a878e8c1..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js +++ /dev/null @@ -1,61 +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 expect from '@kbn/expect'; -import * as and from '../and'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); -const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - -describe('kuery functions', function () { - describe('and', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { - const result = and.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s filter clause', function () { - const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]); - const result = and.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('filter'); - expect(result.bool.filter).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) - ); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js b/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js deleted file mode 100644 index ee4cfab94e614..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js +++ /dev/null @@ -1,92 +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 expect from '@kbn/expect'; -import * as exists from '../exists'; -import { nodeTypes } from '../../node_types'; -import _ from 'lodash'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - - -let indexPattern; - -describe('kuery functions', function () { - describe('exists', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - it('should return a single "arguments" param', function () { - const result = exists.buildNodeParams('response'); - expect(result).to.only.have.key('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const { arguments: [ arg ] } = exists.buildNodeParams('response'); - expect(arg).to.have.property('type', 'literal'); - expect(arg).to.have.property('value', 'response'); - }); - }); - - describe('toElasticsearchQuery', function () { - it('should return an ES exists query', function () { - const expected = { - exists: { field: 'response' } - }; - - const existsNode = nodeTypes.function.buildNode('exists', 'response'); - const result = exists.toElasticsearchQuery(existsNode, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); - }); - - it('should return an ES exists query without an index pattern', function () { - const expected = { - exists: { field: 'response' } - }; - - const existsNode = nodeTypes.function.buildNode('exists', 'response'); - const result = exists.toElasticsearchQuery(existsNode); - expect(_.isEqual(expected, result)).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const existsNode = nodeTypes.function.buildNode('exists', 'script string'); - expect(exists.toElasticsearchQuery) - .withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - exists: { field: 'nestedField.response' } - }; - - const existsNode = nodeTypes.function.buildNode('exists', 'response'); - const result = exists.toElasticsearchQuery( - existsNode, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(_.isEqual(expected, result)).to.be(true); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js deleted file mode 100644 index 7afa0fcce1bfe..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js +++ /dev/null @@ -1,120 +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 expect from '@kbn/expect'; -import * as geoBoundingBox from '../geo_bounding_box'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; -const params = { - bottomRight: { - lat: 50.73, - lon: -135.35 - }, - topLeft: { - lat: 73.12, - lon: -174.37 - } -}; - -describe('kuery functions', function () { - describe('geoBoundingBox', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ , ...args ] } = result; - - args.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - - const expectedParam = params[param.name]; - const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`; - expect(param.value.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_bounding_box query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should return an ES geo_bounding_box query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result.geo_bounding_box.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); - expect(geoBoundingBox.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box).to.have.property('nestedField.geo'); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js deleted file mode 100644 index c1f2fae0bb3e1..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js +++ /dev/null @@ -1,131 +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 expect from '@kbn/expect'; -import * as geoPolygon from '../geo_polygon'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - - -let indexPattern; -const points = [ - { - lat: 69.77, - lon: -171.56 - }, - { - lat: 50.06, - lon: -169.10 - }, - { - lat: 69.16, - lon: -125.85 - } -]; - -describe('kuery functions', function () { - - describe('geoPolygon', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoPolygon.buildNodeParams('geo', points); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided points literal "lat, lon" string values', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ , ...args ] } = result; - - args.forEach((param, index) => { - expect(param).to.have.property('type', 'literal'); - const expectedPoint = points[index]; - const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; - expect(param.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_polygon query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should return an ES geo_polygon query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result.geo_polygon.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); - expect(geoPolygon.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon).to.have.property('nestedField.geo'); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js deleted file mode 100644 index b2f3d7ec16a65..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js +++ /dev/null @@ -1,310 +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 expect from '@kbn/expect'; -import * as is from '../is'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery functions', function () { - - describe('is', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('fieldName and value should be required arguments', function () { - expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/); - expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/); - }); - - it('arguments should contain the provided fieldName and value as literals', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('response', 200); - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'response'); - - expect(value).to.have.property('type', 'literal'); - expect(value).to.have.property('value', 200); - }); - - it('should detect wildcards in the provided arguments', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('machine*', 'win*'); - - expect(fieldName).to.have.property('type', 'wildcard'); - expect(value).to.have.property('type', 'wildcard'); - }); - - it('should default to a non-phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200); - expect(isPhrase.value).to.be(false); - }); - - it('should allow specification of a phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200, true); - expect(isPhrase.value).to.be(true); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES match_all query when fieldName and value are both "*"', function () { - const expected = { - match_all: {} - }; - - const node = nodeTypes.function.buildNode('is', '*', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES multi_match query using default_field when fieldName is null', function () { - const expected = { - multi_match: { - query: 200, - type: 'best_fields', - lenient: true, - } - }; - - const node = nodeTypes.function.buildNode('is', null, 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', function () { - const expected = { - query_string: { - query: 'jpg*', - } - }; - - const node = nodeTypes.function.buildNode('is', null, 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES bool query with a sub-query for each field when fieldName is "*"', function () { - const node = nodeTypes.function.buildNode('is', '*', 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('bool'); - expect(result.bool.should).to.have.length(indexPattern.fields.length); - }); - - it('should return an ES exists query when value is "*"', function () { - const expected = { - bool: { - should: [ - { exists: { field: 'extension' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided without an index pattern', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node); - expect(result).to.eql(expected); - }); - - it('should support creation of phrase queries', function () { - const expected = { - bool: { - should: [ - { match_phrase: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should create a query_string query for wildcard values', function () { - const expected = { - bool: { - should: [ - { - query_string: { - fields: ['extension'], - query: 'jpg*' - } - }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support scripted fields', function () { - const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); - }); - - it('should support date fields without a dateFormat provided', function () { - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support date fields with a dateFormat provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - time_zone: 'America/Phoenix', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - bool: { - should: [ - { match: { 'nestedField.extension': 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.eql(expected); - }); - - it('should support wildcard field names', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should automatically add a nested query when a wildcard field name covers a nested field', () => { - const expected = { - bool: { - should: [ - { - nested: { - path: 'nestedField.nestedChild', - query: { - match: { - 'nestedField.nestedChild.doublyNestedChild': 'foo' - } - }, - score_mode: 'none' - } - } - ], - minimum_should_match: 1 - } - }; - - - const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js b/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js deleted file mode 100644 index 5ba73e485ddf1..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js +++ /dev/null @@ -1,68 +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 expect from '@kbn/expect'; -import * as nested from '../nested'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); - -describe('kuery functions', function () { - describe('nested', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { - const result = nested.buildNodeParams('nestedField', childNode); - const { arguments: [ resultPath, resultChildNode ] } = result; - expect(ast.toElasticsearchQuery(resultPath)).to.be('nestedField'); - expect(resultChildNode).to.be(childNode); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES nested query', function () { - const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); - const result = nested.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('nested'); - expect(result.nested.path).to.be('nestedField'); - expect(result.nested.score_mode).to.be('none'); - }); - - it('should pass the nested path to subqueries so the full field name can be used', function () { - const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); - const result = nested.toElasticsearchQuery(node, indexPattern); - const expectedSubQuery = ast.toElasticsearchQuery( - nodeTypes.function.buildNode('is', 'nestedField.child', 'foo') - ); - expect(result.nested.query).to.eql(expectedSubQuery); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/packages/kbn-es-query/src/kuery/functions/__tests__/not.js deleted file mode 100644 index 7a2d7fa39c152..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js +++ /dev/null @@ -1,60 +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 expect from '@kbn/expect'; -import * as not from '../not'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - -describe('kuery functions', function () { - - describe('not', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child node', function () { - const { arguments: [ actualChild ] } = not.buildNodeParams(childNode); - expect(actualChild).to.be(childNode); - }); - - - }); - - describe('toElasticsearchQuery', function () { - - it('should wrap a subquery in an ES bool query\'s must_not clause', function () { - const node = nodeTypes.function.buildNode('not', childNode); - const result = not.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('must_not'); - expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern)); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/packages/kbn-es-query/src/kuery/functions/__tests__/or.js deleted file mode 100644 index f24f24b98e7fb..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js +++ /dev/null @@ -1,72 +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 expect from '@kbn/expect'; -import * as or from '../or'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); -const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - -describe('kuery functions', function () { - - describe('or', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { - const result = or.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s should clause', function () { - const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); - const result = or.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.have.keys('should'); - expect(result.bool.should).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) - ); - }); - - it('should require one of the clauses to match', function () { - const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); - const result = or.toElasticsearchQuery(node, indexPattern); - expect(result.bool).to.have.property('minimum_should_match', 1); - }); - - }); - - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/packages/kbn-es-query/src/kuery/functions/__tests__/range.js deleted file mode 100644 index 2361e8bb66769..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js +++ /dev/null @@ -1,240 +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 expect from '@kbn/expect'; -import * as range from '../range'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery functions', function () { - - describe('range', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('arguments should contain the provided fieldName as a literal', function () { - const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 }); - const { arguments: [fieldName] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'bytes'); - }); - - it('arguments should contain the provided params as named arguments', function () { - const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; - const result = range.buildNodeParams('bytes', givenParams); - const { arguments: [, ...params] } = result; - - expect(params).to.be.an('array'); - expect(params).to.not.be.empty(); - - params.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - expect(param.value.value).to.be(givenParams[param.name]); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES range query for the node\'s field and params', function () { - const expected = { - bool: { - should: [ - { - range: { - bytes: { - gt: 1000, - lt: 8000 - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES range query without an index pattern', function () { - const expected = { - bool: { - should: [ - { - range: { - bytes: { - gt: 1000, - lt: 8000 - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery(node); - expect(result).to.eql(expected); - }); - - it('should support wildcard field names', function () { - const expected = { - bool: { - should: [ - { - range: { - bytes: { - gt: 1000, - lt: 8000 - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', 'byt*', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support scripted fields', function () { - const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); - }); - - it('should support date fields without a dateFormat provided', function () { - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gt: '2018-01-03T19:04:17', - lt: '2018-04-03T19:04:17', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); - const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support date fields with a dateFormat provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gt: '2018-01-03T19:04:17', - lt: '2018-04-03T19:04:17', - time_zone: 'America/Phoenix', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); - const result = range.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - bool: { - should: [ - { - range: { - 'nestedField.bytes': { - gt: 1000, - lt: 8000 - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.eql(expected); - }); - - it('should automatically add a nested query when a wildcard field name covers a nested field', function () { - const expected = { - bool: { - should: [ - { - nested: { - path: 'nestedField.nestedChild', - query: { - range: { - 'nestedField.nestedChild.doublyNestedChild': { - gt: 1000, - lt: 8000 - } - } - }, - score_mode: 'none' - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('range', '*doublyNested*', { gt: 1000, lt: 8000 }); - const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js b/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js deleted file mode 100644 index 7718479130a8a..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js +++ /dev/null @@ -1,97 +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 { getFields } from '../../utils/get_fields'; -import expect from '@kbn/expect'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; - -import { nodeTypes } from '../../..'; - -let indexPattern; - -describe('getFields', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('field names without a wildcard', function () { - - it('should return an empty array if the field does not exist in the index pattern', function () { - const fieldNameNode = nodeTypes.literal.buildNode('nonExistentField'); - const expected = []; - const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); - }); - - it('should return the single matching field in an array', function () { - const fieldNameNode = nodeTypes.literal.buildNode('extension'); - const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('extension'); - }); - - it('should not match a wildcard in a literal node', function () { - const indexPatternWithWildField = { - title: 'wildIndex', - fields: [ - { - name: 'foo*', - }, - ], - }; - - const fieldNameNode = nodeTypes.literal.buildNode('foo*'); - const results = getFields(fieldNameNode, indexPatternWithWildField); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('foo*'); - - // ensure the wildcard is not actually being parsed - const expected = []; - const actual = getFields(nodeTypes.literal.buildNode('fo*'), indexPatternWithWildField); - expect(actual).to.eql(expected); - }); - }); - - describe('field name patterns with a wildcard', function () { - - it('should return an empty array if it does not match any fields in the index pattern', function () { - const fieldNameNode = nodeTypes.wildcard.buildNode('nonExistent*'); - const expected = []; - const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); - }); - - it('should return all fields that match the pattern in an array', function () { - const fieldNameNode = nodeTypes.wildcard.buildNode('machine*'); - const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(2); - expect(results.find((field) => { - return field.name === 'machine.os'; - })).to.be.ok(); - expect(results.find((field) => { - return field.name === 'machine.os.raw'; - })).to.be.ok(); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js b/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js deleted file mode 100644 index dae15979a161c..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js +++ /dev/null @@ -1,88 +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 expect from '@kbn/expect'; -import { nodeTypes } from '../../../node_types'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; -import { getFullFieldNameNode } from '../../utils/get_full_field_name_node'; - -let indexPattern; - -describe('getFullFieldNameNode', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - it('should return unchanged name node if no nested path is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('notNested'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should add the nested path if it is valid according to the index pattern', () => { - const nameNode = nodeTypes.literal.buildNode('child'); - const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); - expect(result).to.eql(nodeTypes.literal.buildNode('nestedField.child')); - }); - - it('should throw an error if a path is provided for a non-nested field', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'machine') - .to - .throwException(/machine.os is not a nested field but is in nested group "machine" in the KQL expression/); - }); - - it('should throw an error if a nested field is not passed with a path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedField.child'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern) - .to - .throwException(/nestedField.child is a nested field, but is not in a nested group in the KQL expression./); - }); - - it('should throw an error if a nested field is passed with the wrong path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'nestedField') - .to - // eslint-disable-next-line max-len - .throwException(/Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/); - }); - - it('should skip error checking for wildcard names', () => { - const nameNode = nodeTypes.wildcard.buildNode('nested*'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should skip error checking if no index pattern is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, null, 'machine') - .to - .not - .throwException(); - - const result = getFullFieldNameNode(nameNode, null, 'machine'); - expect(result).to.eql(nodeTypes.literal.buildNode('machine.os')); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js deleted file mode 100644 index 63ade9e8793a7..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ /dev/null @@ -1,171 +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 _ from 'lodash'; -import * as ast from '../ast'; -import * as literal from '../node_types/literal'; -import * as wildcard from '../node_types/wildcard'; -import { getPhraseScript } from '../../utils/filters'; -import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; -import { getFullFieldNameNode } from './utils/get_full_field_name_node'; - -export function buildNodeParams(fieldName, value, isPhrase = false) { - if (_.isUndefined(fieldName)) { - throw new Error('fieldName is a required argument'); - } - if (_.isUndefined(value)) { - throw new Error('value is a required argument'); - } - const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); - const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value); - const isPhraseNode = literal.buildNode(isPhrase); - return { - arguments: [fieldNode, valueNode, isPhraseNode], - }; -} - -export function toElasticsearchQuery(node, indexPattern = null, config = {}, context = {}) { - const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; - const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); - const fieldName = ast.toElasticsearchQuery(fullFieldNameArg); - const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; - const type = isPhraseArg.value ? 'phrase' : 'best_fields'; - if (fullFieldNameArg.value === null) { - if (valueArg.type === 'wildcard') { - return { - query_string: { - query: wildcard.toQueryStringQuery(valueArg), - }, - }; - } - - return { - multi_match: { - type, - query: value, - lenient: true, - } - }; - } - - const fields = indexPattern ? getFields(fullFieldNameArg, indexPattern) : []; - // If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve - // the behaviour of lucene on dashboards where there are panels based on different index patterns that have different - // fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the - // field should return no results. It's debatable whether this is desirable, but it's been that way forever, so we'll - // keep things familiar for now. - if (fields && fields.length === 0) { - fields.push({ - name: ast.toElasticsearchQuery(fullFieldNameArg), - scripted: false, - }); - } - - const isExistsQuery = valueArg.type === 'wildcard' && value === '*'; - const isAllFieldsQuery = - (fullFieldNameArg.type === 'wildcard' && fieldName === '*') - || (fields && indexPattern && fields.length === indexPattern.fields.length); - const isMatchAllQuery = isExistsQuery && isAllFieldsQuery; - - if (isMatchAllQuery) { - return { match_all: {} }; - } - - const queries = fields.reduce((accumulator, field) => { - const wrapWithNestedQuery = (query) => { - // Wildcards can easily include nested and non-nested fields. There isn't a good way to let - // users handle this themselves so we automatically add nested queries in this scenario. - if ( - !(fullFieldNameArg.type === 'wildcard') - || !_.get(field, 'subType.nested') - || context.nested - ) { - return query; - } - else { - return { - nested: { - path: field.subType.nested.path, - query, - score_mode: 'none' - } - }; - } - }; - - if (field.scripted) { - // Exists queries don't make sense for scripted fields - if (!isExistsQuery) { - return [...accumulator, { - script: { - ...getPhraseScript(field, value) - } - }]; - } - } - else if (isExistsQuery) { - return [...accumulator, wrapWithNestedQuery({ - exists: { - field: field.name - } - })]; - } - else if (valueArg.type === 'wildcard') { - return [...accumulator, wrapWithNestedQuery({ - query_string: { - fields: [field.name], - query: wildcard.toQueryStringQuery(valueArg), - } - })]; - } - /* - If we detect that it's a date field and the user wants an exact date, we need to convert the query to both >= and <= the value provided to force a range query. This is because match and match_phrase queries do not accept a timezone parameter. - dateFormatTZ can have the value of 'Browser', in which case we guess the timezone using moment.tz.guess. - */ - else if (field.type === 'date') { - const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; - return [...accumulator, wrapWithNestedQuery({ - range: { - [field.name]: { - gte: value, - lte: value, - ...timeZoneParam, - }, - } - })]; - } - else { - const queryType = type === 'phrase' ? 'match_phrase' : 'match'; - return [...accumulator, wrapWithNestedQuery({ - [queryType]: { - [field.name]: value - } - })]; - } - }, []); - - return { - bool: { - should: queries, - minimum_should_match: 1 - } - }; -} - diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/packages/kbn-es-query/src/kuery/functions/range.js deleted file mode 100644 index f7719998ad524..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/range.js +++ /dev/null @@ -1,130 +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 _ from 'lodash'; -import { nodeTypes } from '../node_types'; -import * as ast from '../ast'; -import { getRangeScript } from '../../utils/filters'; -import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; -import { getFullFieldNameNode } from './utils/get_full_field_name_node'; - -export function buildNodeParams(fieldName, params) { - params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format'); - const fieldNameArg = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : nodeTypes.literal.buildNode(fieldName); - const args = _.map(params, (value, key) => { - return nodeTypes.namedArg.buildNode(key, value); - }); - - return { - arguments: [fieldNameArg, ...args], - }; -} - -export function toElasticsearchQuery(node, indexPattern = null, config = {}, context = {}) { - const [ fieldNameArg, ...args ] = node.arguments; - const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); - const fields = indexPattern ? getFields(fullFieldNameArg, indexPattern) : []; - const namedArgs = extractArguments(args); - const queryParams = _.mapValues(namedArgs, ast.toElasticsearchQuery); - - // If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve - // the behaviour of lucene on dashboards where there are panels based on different index patterns that have different - // fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the - // field should return no results. It's debatable whether this is desirable, but it's been that way forever, so we'll - // keep things familiar for now. - if (fields && fields.length === 0) { - fields.push({ - name: ast.toElasticsearchQuery(fullFieldNameArg), - scripted: false, - }); - } - - - const queries = fields.map((field) => { - const wrapWithNestedQuery = (query) => { - // Wildcards can easily include nested and non-nested fields. There isn't a good way to let - // users handle this themselves so we automatically add nested queries in this scenario. - if ( - !fullFieldNameArg.type === 'wildcard' - || !_.get(field, 'subType.nested') - || context.nested - ) { - return query; - } - else { - return { - nested: { - path: field.subType.nested.path, - query, - score_mode: 'none' - } - }; - } - }; - - if (field.scripted) { - return { - script: getRangeScript(field, queryParams), - }; - } - else if (field.type === 'date') { - const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; - return wrapWithNestedQuery({ - range: { - [field.name]: { - ...queryParams, - ...timeZoneParam, - } - } - }); - } - return wrapWithNestedQuery({ - range: { - [field.name]: queryParams - } - }); - }); - - return { - bool: { - should: queries, - minimum_should_match: 1 - } - }; -} - -function extractArguments(args) { - if ((args.gt && args.gte) || (args.lt && args.lte)) { - throw new Error('range ends cannot be both inclusive and exclusive'); - } - - const unnamedArgOrder = ['gte', 'lte', 'format']; - - return args.reduce((acc, arg, index) => { - if (arg.type === 'namedArg') { - acc[arg.name] = arg.value; - } - else { - acc[unnamedArgOrder[index]] = arg; - } - - return acc; - }, {}); -} diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/packages/kbn-es-query/src/kuery/index.d.ts deleted file mode 100644 index b01a8914f68ef..0000000000000 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ /dev/null @@ -1,30 +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. - */ - -export * from './ast'; -export { nodeTypes } from './node_types'; - -export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -export interface JsonObject { - [key: string]: JsonValue; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/packages/kbn-es-query/src/kuery/index.js b/packages/kbn-es-query/src/kuery/index.js deleted file mode 100644 index e0cacada7f274..0000000000000 --- a/packages/kbn-es-query/src/kuery/index.js +++ /dev/null @@ -1,22 +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. - */ - -export * from './ast'; -export { nodeTypes } from './node_types'; -export * from './errors'; diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js deleted file mode 100644 index de00c083fc830..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js +++ /dev/null @@ -1,80 +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 * as functionType from '../function'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import * as isFunction from '../../functions/is'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('function', function () { - - let indexPattern; - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNode', function () { - - it('should return a node representing the given kuery function', function () { - const result = functionType.buildNode('is', 'extension', 'jpg'); - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - }); - - }); - - describe('buildNodeWithArgumentNodes', function () { - - it('should return a function node with the given argument list untouched', function () { - const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); - const valueLiteral = nodeTypes.literal.buildNode('jpg'); - const argumentNodes = [fieldNameLiteral, valueLiteral]; - const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes); - - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - expect(result.arguments).to.be(argumentNodes); - expect(result.arguments).to.eql(argumentNodes); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given function type\'s ES query representation', function () { - const node = functionType.buildNode('is', 'extension', 'jpg'); - const expected = isFunction.toElasticsearchQuery(node, indexPattern); - const result = functionType.toElasticsearchQuery(node, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); - }); - - }); - - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js deleted file mode 100644 index 25fe2bcc45a45..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js +++ /dev/null @@ -1,50 +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 expect from '@kbn/expect'; -import * as literal from '../literal'; - -describe('kuery node types', function () { - - describe('literal', function () { - - describe('buildNode', function () { - - it('should return a node representing the given value', function () { - const result = literal.buildNode('foo'); - expect(result).to.have.property('type', 'literal'); - expect(result).to.have.property('value', 'foo'); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the literal value represented by the given node', function () { - const node = literal.buildNode('foo'); - const result = literal.toElasticsearchQuery(node); - expect(result).to.be('foo'); - }); - - }); - - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js deleted file mode 100644 index cfb8f6d5274db..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js +++ /dev/null @@ -1,62 +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 expect from '@kbn/expect'; -import * as namedArg from '../named_arg'; -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('named arg', function () { - - describe('buildNode', function () { - - it('should return a node representing a named argument with the given value', function () { - const result = namedArg.buildNode('fieldName', 'foo'); - expect(result).to.have.property('type', 'namedArg'); - expect(result).to.have.property('name', 'fieldName'); - expect(result).to.have.property('value'); - - const literalValue = result.value; - expect(literalValue).to.have.property('type', 'literal'); - expect(literalValue).to.have.property('value', 'foo'); - }); - - it('should support literal nodes as values', function () { - const value = nodeTypes.literal.buildNode('foo'); - const result = namedArg.buildNode('fieldName', value); - expect(result.value).to.be(value); - expect(result.value).to.eql(value); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the argument value represented by the given node', function () { - const node = namedArg.buildNode('fieldName', 'foo'); - const result = namedArg.toElasticsearchQuery(node); - expect(result).to.be('foo'); - }); - - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js deleted file mode 100644 index 0c4379378c6d6..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js +++ /dev/null @@ -1,107 +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 expect from '@kbn/expect'; -import * as wildcard from '../wildcard'; - -describe('kuery node types', function () { - - describe('wildcard', function () { - - describe('buildNode', function () { - - it('should accept a string argument representing a wildcard string', function () { - const wildcardValue = `foo${wildcard.wildcardSymbol}bar`; - const result = wildcard.buildNode(wildcardValue); - expect(result).to.have.property('type', 'wildcard'); - expect(result).to.have.property('value', wildcardValue); - }); - - it('should accept and parse a wildcard string', function () { - const result = wildcard.buildNode('foo*bar'); - expect(result).to.have.property('type', 'wildcard'); - expect(result.value).to.be(`foo${wildcard.wildcardSymbol}bar`); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toElasticsearchQuery(node); - expect(result).to.be('foo*bar'); - }); - - }); - - describe('toQueryStringQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('foo*bar'); - }); - - it('should escape query_string query special characters other than wildcard', function () { - const node = wildcard.buildNode('+foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('\\+foo*bar'); - }); - - }); - - describe('test', function () { - - it('should return a boolean indicating whether the string matches the given wildcard node', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foobar')).to.be(true); - expect(wildcard.test(node, 'foobazbar')).to.be(true); - expect(wildcard.test(node, 'foobar')).to.be(true); - - expect(wildcard.test(node, 'fooqux')).to.be(false); - expect(wildcard.test(node, 'bazbar')).to.be(false); - }); - - it('should return a true even when the string has newlines or tabs', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foo\nbar')).to.be(true); - expect(wildcard.test(node, 'foo\tbar')).to.be(true); - }); - }); - - describe('hasLeadingWildcard', function () { - it('should determine whether a wildcard node contains a leading wildcard', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.hasLeadingWildcard(node)).to.be(false); - - const leadingWildcardNode = wildcard.buildNode('*foobar'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(true); - }); - - // Lone wildcards become exists queries, so we aren't worried about their performance - it('should not consider a lone wildcard to be a leading wildcard', function () { - const leadingWildcardNode = wildcard.buildNode('*'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(false); - }); - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/packages/kbn-es-query/src/kuery/node_types/index.d.ts deleted file mode 100644 index daf8032f9fe0e..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/index.d.ts +++ /dev/null @@ -1,77 +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. - */ - -/** - * WARNING: these typings are incomplete - */ - -import { JsonObject, JsonValue } from '..'; - -type FunctionName = - | 'is' - | 'and' - | 'or' - | 'not' - | 'range' - | 'exists' - | 'geoBoundingBox' - | 'geoPolygon' - | 'nested'; - -interface FunctionTypeBuildNode { - type: 'function'; - function: FunctionName; - // TODO -> Need to define a better type for DSL query - arguments: any[]; -} - -interface FunctionType { - buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; -} - -interface LiteralType { - buildNode: ( - value: null | boolean | number | string - ) => { type: 'literal'; value: null | boolean | number | string }; - toElasticsearchQuery: (node: any) => null | boolean | number | string; -} - -interface NamedArgType { - buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; - toElasticsearchQuery: (node: any) => string; -} - -interface WildcardType { - buildNode: (value: string) => { type: 'wildcard'; value: string }; - test: (node: any, string: string) => boolean; - toElasticsearchQuery: (node: any) => string; - toQueryStringQuery: (node: any) => string; - hasLeadingWildcard: (node: any) => boolean; -} - -interface NodeTypes { - function: FunctionType; - literal: LiteralType; - namedArg: NamedArgType; - wildcard: WildcardType; -} - -export const nodeTypes: NodeTypes; diff --git a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js deleted file mode 100644 index 6deaccadfdb76..0000000000000 --- a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js +++ /dev/null @@ -1,36 +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 expect from '@kbn/expect'; -import { getTimeZoneFromSettings } from '../get_time_zone_from_settings'; - -describe('get timezone from settings', function () { - - it('should return the config timezone if the time zone is set', function () { - const result = getTimeZoneFromSettings('America/Chicago'); - expect(result).to.eql('America/Chicago'); - }); - - it('should return the system timezone if the time zone is set to "Browser"', function () { - const result = getTimeZoneFromSettings('Browser'); - expect(result).to.not.equal('Browser'); - }); - -}); - diff --git a/packages/kbn-es-query/src/utils/filters.js b/packages/kbn-es-query/src/utils/filters.js deleted file mode 100644 index 6e4f5c342688c..0000000000000 --- a/packages/kbn-es-query/src/utils/filters.js +++ /dev/null @@ -1,133 +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 { pick, get, reduce, map } from 'lodash'; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const getConvertedValueForField = (field, value) => { - if (typeof value !== 'boolean' && field.type === 'boolean') { - if ([1, 'true'].includes(value)) { - return true; - } else if ([0, 'false'].includes(value)) { - return false; - } else { - throw new Error(`${value} is not a valid boolean value for boolean field ${field.name}`); - } - } - return value; -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const buildInlineScriptForPhraseFilter = (scriptedField) => { - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (scriptedField.lang === 'painless') { - return ( - `boolean compare(Supplier s, def v) {return s.get() == v;}` + - `compare(() -> { ${scriptedField.script} }, params.value);` - ); - } else { - return `(${scriptedField.script}) == value`; - } -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export function getPhraseScript(field, value) { - const convertedValue = getConvertedValueForField(field, value); - const script = buildInlineScriptForPhraseFilter(field); - - return { - script: { - source: script, - lang: field.lang, - params: { - value: convertedValue, - }, - }, - }; -} - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/range_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'kuery' into new platform - * */ -export function getRangeScript(field, params) { - const operators = { - gt: '>', - gte: '>=', - lte: '<=', - lt: '<', - }; - const comparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get() > v}', - gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}', - lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}', - lt: 'boolean lt(Supplier s, def v) {return s.get() < v}', - }; - - const dateComparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get().toInstant().isAfter(Instant.parse(v))}', - gte: 'boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))}', - lte: 'boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}', - lt: 'boolean lt(Supplier s, def v) {return s.get().toInstant().isBefore(Instant.parse(v))}', - }; - - const knownParams = pick(params, (val, key) => { - return key in operators; - }); - let script = map(knownParams, (val, key) => { - return '(' + field.script + ')' + get(operators, key) + key; - }).join(' && '); - - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (field.lang === 'painless') { - const comp = field.type === 'date' ? dateComparators : comparators; - const currentComparators = reduce( - knownParams, - (acc, val, key) => acc.concat(get(comp, key)), - [] - ).join(' '); - - const comparisons = map(knownParams, (val, key) => { - return `${key}(() -> { ${field.script} }, params.${key})`; - }).join(' && '); - - script = `${currentComparators}${comparisons}`; - } - - return { - script: { - source: script, - params: knownParams, - lang: field.lang, - }, - }; -} diff --git a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js deleted file mode 100644 index 1a06941ece127..0000000000000 --- a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js +++ /dev/null @@ -1,28 +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 moment from 'moment-timezone'; -const detectedTimezone = moment.tz.guess(); - -export function getTimeZoneFromSettings(dateFormatTZ) { - if (dateFormatTZ === 'Browser') { - return detectedTimezone; - } - return dateFormatTZ; -} diff --git a/packages/kbn-es-query/src/utils/index.js b/packages/kbn-es-query/src/utils/index.js deleted file mode 100644 index 27f51c1f44cf2..0000000000000 --- a/packages/kbn-es-query/src/utils/index.js +++ /dev/null @@ -1,20 +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. - */ - -export * from './get_time_zone_from_settings'; diff --git a/packages/kbn-es-query/tasks/build_cli.js b/packages/kbn-es-query/tasks/build_cli.js deleted file mode 100644 index 2a43c4d10e007..0000000000000 --- a/packages/kbn-es-query/tasks/build_cli.js +++ /dev/null @@ -1,103 +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. - */ - -const { resolve } = require('path'); - -const getopts = require('getopts'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -const unknownFlags = []; -const flags = getopts(process.argv, { - boolean: ['watch', 'help', 'source-maps'], - unknown(name) { - unknownFlags.push(name); - }, -}); - -const log = new ToolingLog({ - level: pickLevelFromFlags(flags), - writeTo: process.stdout, -}); - -if (unknownFlags.length) { - log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`); - flags.help = true; - process.exitCode = 1; -} - -if (flags.help) { - log.info(` - Simple build tool for @kbn/es-query package - - --watch Run in watch mode - --source-maps Include sourcemaps - --help Show this message - `); - process.exit(); -} - -withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['public', 'server'].map(subTask => - proc.run(padRight(12, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.js,.ts,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - cwd, - env: { - ...env, - BABEL_ENV: subTask, - }, - }) - ), - ]); - - log.success('Complete'); -}).catch(error => { - log.error(error); - process.exit(1); -}); diff --git a/packages/kbn-es-query/tsconfig.browser.json b/packages/kbn-es-query/tsconfig.browser.json deleted file mode 100644 index 4a91407471266..0000000000000 --- a/packages/kbn-es-query/tsconfig.browser.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.browser.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/public" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json deleted file mode 100644 index 05f51bbccd2ff..0000000000000 --- a/packages/kbn-es-query/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/server" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/packages/kbn-es/README.md b/packages/kbn-es/README.md index 0d9af2442ebf4..4d4c2aa94db07 100644 --- a/packages/kbn-es/README.md +++ b/packages/kbn-es/README.md @@ -59,4 +59,62 @@ Cloned location of elasticsearch repository, used when running from source Type: `String` -Location where snapshots are cached \ No newline at end of file +Location where snapshots are cached + +## Snapshot Pinning + +Sometimes we need to pin snapshots for a specific version. We'd really like to get this automated, but until that is completed here are the steps to take to build, upload, and switch to pinned snapshots for a branch. + +To use these steps you'll need to setup the google-cloud-sdk, which can be installed on macOS with `brew cask install google-cloud-sdk`. Login with the CLI and you'll have access to the `gsutil` to do efficient/parallel uploads to GCS from the command line. + + 1. Clone the elasticsearch repo somewhere + 2. Checkout the branch you want to build + 3. Run the following to delete old distributables + + ``` + find distribution/archives -type f \( -name 'elasticsearch-*-*.tar.gz' -o -name 'elasticsearch-*-*.zip' \) -not -path *no-jdk* -exec rm {} \; + ``` + + 4. Build the new artifacts + + ``` + ./gradlew -p distribution/archives assemble --parallel + ``` + + 4. Copy new artifacts to your `~/Downloads/tmp-artifacts` + + ``` + rm -rf ~/Downloads/tmp-artifacts + mkdir ~/Downloads/tmp-artifacts + find distribution/archives -type f \( -name 'elasticsearch-*-*.tar.gz' -o -name 'elasticsearch-*-*.zip' \) -not -path *no-jdk* -exec cp {} ~/Downloads/tmp-artifacts \; + ``` + + 5. Calculate shasums of the uploads + + ``` + cd ~/Downloads/tmp-artifacts + find * -exec bash -c "shasum -a 512 {} > {}.sha512" \; + ``` + + 6. Check that the files in `~/Downloads/tmp-artifacts` look reasonable + 7. Upload the files to GCS + + ``` + gsutil -m rsync . gs://kibana-ci-tmp-artifacts/ + ``` + + 8. Once the artifacts are uploaded, modify `packages/kbn-es/src/custom_snapshots.js` in a PR to use a URL formatted like: + + ``` + // force use of manually created snapshots until ReindexPutMappings fix + if (!process.env.KBN_ES_SNAPSHOT_URL && !process.argv.some(isVersionFlag)) { + // return undefined; + return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + } + ``` + + For 6.8, the format of the url should look like: + + ``` + 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}.{ext}'; + ``` \ No newline at end of file diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 3e25ceb8714df..0146111941044 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -28,7 +28,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", "prop-types": "^15.6.2", - "react": "^16.6.0", + "react": "^16.8.6", "react-intl": "^2.8.0" } } diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index b43dcd80b4462..aa6611f3b6738 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -70,11 +70,21 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug describe(`then running`, () => { it(`'yarn test:browser' should exit 0`, async () => { - await execa('yarn', ['test:browser'], { cwd: generatedPath }); + await execa('yarn', ['test:browser'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { cwd: generatedPath }); + await execa('yarn', ['test:server'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn build' should exit 0`, async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 52672d5f039fb..4530b61423620 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -143,7 +143,7 @@ export const schema = Joi.object() junit: Joi.object() .keys({ - enabled: Joi.boolean().default(!!process.env.CI), + enabled: Joi.boolean().default(!!process.env.CI && !process.env.DISABLE_JUNIT_REPORTER), reportName: Joi.string(), }) .default(), diff --git a/packages/kbn-test/src/mocha/auto_junit_reporter.js b/packages/kbn-test/src/mocha/auto_junit_reporter.js index 50b589fbc57a5..b6e79616e1cde 100644 --- a/packages/kbn-test/src/mocha/auto_junit_reporter.js +++ b/packages/kbn-test/src/mocha/auto_junit_reporter.js @@ -29,7 +29,7 @@ export function createAutoJUnitReporter(junitReportOptions) { new MochaSpecReporter(runner, options); // in CI we also setup the JUnit reporter - if (process.env.CI) { + if (process.env.CI && !process.env.DISABLE_JUNIT_REPORTER) { setupJUnitReportGeneration(runner, junitReportOptions); } } diff --git a/packages/kbn-test/src/mocha/run_mocha_cli.js b/packages/kbn-test/src/mocha/run_mocha_cli.js index 7a90108472721..77f40aded1d7f 100644 --- a/packages/kbn-test/src/mocha/run_mocha_cli.js +++ b/packages/kbn-test/src/mocha/run_mocha_cli.js @@ -63,7 +63,16 @@ export function runMochaCli() { if (!opts._.length) { globby .sync( - ['src/**/__tests__/**/*.js', 'packages/**/__tests__/**/*.js', 'tasks/**/__tests__/**/*.js'], + [ + 'src/**/__tests__/**/*.js', + 'packages/**/__tests__/**/*.js', + 'tasks/**/__tests__/**/*.js', + 'x-pack/common/**/__tests__/**/*.js', + 'x-pack/server/**/__tests__/**/*.js', + `x-pack/legacy/plugins/*/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/common/**/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/**/server/**/__tests__/**/*.js`, + ], { cwd: REPO_ROOT, onlyFiles: true, diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index ca594fe44b6c7..ee5424370fb06 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -19,7 +19,7 @@ "focus-trap-react": "^3.1.1", "lodash": "npm:@elastic/lodash@3.10.1-kibana3", "prop-types": "15.6.0", - "react": "^16.2.0", + "react": "^16.8.6", "react-ace": "^5.9.0", "react-color": "^2.13.8", "tabbable": "1.1.3", @@ -57,7 +57,7 @@ "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", - "react-dom": "^16.2.0", + "react-dom": "^16.8.6", "react-redux": "^5.0.6", "react-router": "^3.2.0", "react-router-redux": "^4.0.8", diff --git a/renovate.json5 b/renovate.json5 index aefbc61e8dc12..3886715618e99 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -21,6 +21,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -28,6 +29,7 @@ major: { labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -228,6 +230,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', diff --git a/rfcs/text/0006_management_section_service.md b/rfcs/text/0006_management_section_service.md index bcb74b1bcd8da..d9781e85cd8a9 100644 --- a/rfcs/text/0006_management_section_service.md +++ b/rfcs/text/0006_management_section_service.md @@ -260,10 +260,10 @@ interface API { PAGE_TITLE_COMPONENT: string; // actually related to advanced settings? PAGE_SUBTITLE_COMPONENT: string; // actually related to advanced settings? PAGE_FOOTER_COMPONENT: string; // actually related to advanced settings? - SidebarNav: React.SFC; + SidebarNav: React.FC; registerSettingsComponent: ( id: string, - component: string | React.SFC, + component: string | React.FC, allowOverride: boolean ) => void; management: new ManagementSection(); diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 9f4e678c6adf5..b65cd3835cc0a 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), - require.resolve('../test/interpreter_functional/config.js'), + require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), ]); diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 11a5f33a1b2d8..fbe2740b96108 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -210,3 +210,40 @@ export class Plugin { } } ``` + +### Usage Collection + +For creating and registering a Usage Collector. Collectors would be defined in a separate directory `server/collectors/register.ts`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 6989c2159dce3..c5e04c3cfb53a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -9,6 +9,7 @@ - [Challenges on the server](#challenges-on-the-server) - [Challenges in the browser](#challenges-in-the-browser) - [Plan of action](#plan-of-action) + - [Shared application plugins](#shared-application-plugins) - [Server-side plan of action](#server-side-plan-of-action) - [De-couple from hapi.js server and request objects](#de-couple-from-hapijs-server-and-request-objects) - [Introduce new plugin definition shim](#introduce-new-plugin-definition-shim) @@ -314,6 +315,43 @@ First, decouple your plugin's business logic from the dependencies that are not Once those things are finished for any given plugin, it can officially be switched to the new plugin system. +### Shared application plugins + +Some services have been already moved to the new platform. + +Below you can find their new locations: + +| Service | Old place | New place in the NP | +| --------------- | ----------------------------------------- | --------------------------------------------------- | +| *FieldFormats* | ui/registry/field_formats | plugins/data/public | + +The `FieldFormats` service has been moved to the `data` plugin in the New Platform. If your plugin has any imports from `ui/registry/field_formats`, you'll need to update your imports as follows: + +Use it in your New Platform plugin: + +```ts +class MyPlugin { + setup (core, { data }) { + data.fieldFormats.register(myFieldFormat); + // ... + } + start (core, { data }) { + data.fieldFormats.getType(myFieldFormatId); + // ... + } +} +``` + +Or, in your legacy platform plugin, consume it through the `ui/new_platform` module: + +```ts +import { npSetup, npStart } from 'ui/new_platform'; + +npSetup.plugins.data.fieldFormats.register(myFieldFormat); +npStart.plugins.data.fieldFormats.getType(myFieldFormatId); +// ... +``` + ## Server-side plan of action Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. This greatly simplifies the plan of action for migrating server-side plugins. @@ -1112,6 +1150,7 @@ import { npStart: { core } } from 'ui/new_platform'; | `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | | `ui/saved_objects` | [`core.savedObjects`](/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md) | Client API is the same | | `ui/doc_title` | [`core.chrome.docTitle`](/docs/development/core/public/kibana-plugin-public.chromedoctitle.md) | | +| `uiExports/injectedVars` | [Configure plugin](#configure-plugin) and [`PluginConfigDescriptor.exposeToBrowser`](/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Can only be used to expose configuration properties | _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ @@ -1128,8 +1167,8 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/apply_filters'` | `import { applyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | @@ -1139,7 +1178,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `ui/embeddable` | `embeddables` | still in progress | | `ui/filter_manager` | `data.filter` | -- | | `ui/index_patterns` | `data.indexPatterns` | still in progress | -| `ui/registry/feature_catalogue | `feature_catalogue.register` | Must add `feature_catalogue` as a dependency in your kibana.json. | +| `ui/registry/feature_catalogue` | `home.featureCatalogue.register` | Must add `home` as a dependency in your kibana.json. | | `ui/registry/vis_types` | `visualizations.types` | -- | | `ui/vis` | `visualizations.types` | -- | | `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` | @@ -1182,7 +1221,7 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `fieldFormatEditors` | | | | `fieldFormats` | | | | `hacks` | n/a | Just run the code in your plugin's `start` method. | -| `home` | [`plugins.feature_catalogue.register`](./src/plugins/feature_catalogue) | Must add `feature_catalogue` as a dependency in your kibana.json. | +| `home` | [`plugins.home.featureCatalogue.register`](./src/plugins/home/public/feature_catalogue) | Must add `home` as a dependency in your kibana.json. | | `indexManagement` | | Should be an API on the indexManagement plugin. | | `injectDefaultVars` | n/a | Plugins will only be able to "whitelist" config values for the frontend. See [#41990](https://github.com/elastic/kibana/issues/41990) | | `inspectorViews` | | Should be an API on the data (?) plugin. | @@ -1244,6 +1283,43 @@ class MyPlugin { } ``` +If your plugin also have a client-side part, you can also expose configuration properties to it using a whitelisting mechanism with the configuration `exposeToBrowser` property. +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + uiProp: true, + }, + schema: configSchema, +}; +``` + +Configuration containing only the exposed properties will be then available on the client-side using the plugin's `initializerContext`: +```typescript +// my_plugin/public/index.ts +interface ClientConfigType { + uiProp: string; +} + +export class Plugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = this.initializerContext.config.get(); + // ... + } +``` + ### Mock new platform services in tests #### Writing mocks for your plugin diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9eed3a59acaa6..ccf14879baa37 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -298,6 +298,35 @@ class Plugin { } } ``` +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` +as a temporary solution until error migration is complete: +```ts +// legacy/plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; + +export interface DemoPluginsSetup {}; + +class Plugin { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + router.wrapErrors((context, req, res) => { + throw Boom.notFound('not there'); // will be converted into proper New Platform error + }) + ) + } +} +``` + #### 4. New Platform plugin As the final step we delete the shim and move all our code into a New Platform diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index e6a1070e1a684..593858851d387 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -18,7 +18,6 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; import { mount, ReactWrapper } from 'enzyme'; import { createMemoryHistory, History } from 'history'; import { BehaviorSubject } from 'rxjs'; @@ -31,13 +30,8 @@ import { AppRouter, AppNotFound } from '../ui'; const createMountHandler = (htmlString: string) => jest.fn(async ({ appBasePath: basename, element: el }: AppMountParameters) => { - ReactDOM.render( -

, - el - ); - return jest.fn(() => ReactDOM.unmountComponentAtNode(el)); + el.innerHTML = `
\nbasename: ${basename}\nhtml: ${htmlString}\n
`; + return jest.fn(() => (el.innerHTML = '')); }); describe('AppContainer', () => { diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 9d8acf1978556..b574bf16278e2 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -39,7 +39,7 @@ interface Props { redirectTo?: (path: string) => void; } -export const AppRouter: React.StatelessComponent = ({ +export const AppRouter: React.FunctionComponent = ({ history, redirectTo = (path: string) => (window.location.href = path), ...otherProps diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 3390480e56bdd..9656739421686 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -418,17 +418,20 @@ describe('start', () => { .pipe(toArray()) .toPromise(); - chrome.setHelpExtension(() => () => undefined); + chrome.setHelpExtension({ appName: 'App name', content: () => () => undefined }); chrome.setHelpExtension(undefined); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` - Array [ - undefined, - [Function], - undefined, - ] - `); + Array [ + undefined, + Object { + "appName": "App name", + "content": [Function], + }, + undefined, + ] + `); }); }); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index e686f03413dd5..cc23b79e1c621 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -36,6 +36,7 @@ import { NavControlsService, ChromeNavControls } from './nav_controls'; import { DocTitleService, ChromeDocTitle } from './doc_title'; import { LoadingIndicator, HeaderWrapper as Header } from './ui'; import { DocLinksStart } from '../doc_links'; +import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -58,7 +59,20 @@ export interface ChromeBrand { export type ChromeBreadcrumb = EuiBreadcrumb; /** @public */ -export type ChromeHelpExtension = (element: HTMLDivElement) => () => void; +export interface ChromeHelpExtension { + /** + * Provide your plugin's name to create a header for separation + */ + appName: string; + /** + * Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button + */ + links?: ChromeHelpExtensionMenuLink[]; + /** + * Custom content to occur below the list of links + */ + content?: (element: HTMLDivElement) => () => void; +} interface ConstructorParams { browserSupportsCsp: boolean; diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index b220a81f775f8..4a500836990a7 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -26,6 +26,13 @@ export { ChromeBrand, ChromeHelpExtension, } from './chrome_service'; +export { + ChromeHelpExtensionMenuLink, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, +} from './ui/header/header_help_menu'; export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './nav_links'; export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; export { ChromeNavControl, ChromeNavControls } from './nav_controls'; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 1e97899be5854..904618651201f 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -24,21 +24,13 @@ import * as Rx from 'rxjs'; import { // TODO: add type annotations - // @ts-ignore EuiHeader, - // @ts-ignore EuiHeaderLogo, - // @ts-ignore EuiHeaderSection, - // @ts-ignore EuiHeaderSectionItem, - // @ts-ignore EuiHeaderSectionItemButton, - // @ts-ignore - EuiHideFor, EuiHorizontalRule, EuiIcon, - // @ts-ignore EuiImage, // @ts-ignore EuiNavDrawer, diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index c04fbaa07ba71..50659a777eccb 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -18,11 +18,12 @@ */ import * as Rx from 'rxjs'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, + EuiButtonEmptyProps, EuiFlexGroup, EuiFlexItem, EuiHeaderSectionItemButton, @@ -30,8 +31,11 @@ import { EuiPopover, EuiPopoverTitle, EuiSpacer, + EuiTitle, + EuiHorizontalRule, } from '@elastic/eui'; +import { ExclusiveUnion } from '@elastic/eui'; import { HeaderExtension } from './header_extension'; import { ChromeHelpExtension } from '../../chrome_service'; import { @@ -41,6 +45,69 @@ import { KIBANA_FEEDBACK_LINK, } from '../../constants'; +/** @public */ +export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { + /** + * Creates a link to a new github issue in the Kibana repo + */ + linkType: 'github'; + /** + * Include at least one app-specific label to be applied to the new github issue + */ + labels: string[]; + /** + * Provides initial text for the title of the issue + */ + title?: string; +}; + +/** @public */ +export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { + /** + * Creates a generic give feedback link with comment icon + */ + linkType: 'discuss'; + /** + * URL to discuss page. + * i.e. `https://discuss.elastic.co/c/${appName}` + */ + href: string; +}; + +/** @public */ +export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { + /** + * Creates a deep-link to app-specific documentation + */ + linkType: 'documentation'; + /** + * URL to documentation page. + * i.e. `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html`, + */ + href: string; +}; + +/** @public */ +export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { + /** + * Extend EuiButtonEmpty to provide extra functionality + */ + linkType: 'custom'; + /** + * Content of the button (in lieu of `children`) + */ + content: React.ReactNode; +}; + +/** @public */ +export type ChromeHelpExtensionMenuLink = ExclusiveUnion< + ChromeHelpExtensionMenuGitHubLink, + ExclusiveUnion< + ChromeHelpExtensionMenuDiscussLink, + ExclusiveUnion + > +>; + interface Props { helpExtension$: Rx.Observable; intl: InjectedIntl; @@ -84,6 +151,36 @@ class HeaderHelpMenuUI extends Component { } } + createGithubUrl = (labels: string[], title?: string) => { + const url = new URL('https://github.com/elastic/kibana/issues/new?'); + + if (labels.length) { + url.searchParams.set('labels', labels.join(',')); + } + + if (title) { + url.searchParams.set('title', title); + } + + return url.toString(); + }; + + createCustomLink = ( + index: number, + text: React.ReactNode, + addSpacer?: boolean, + buttonProps?: EuiButtonEmptyProps + ) => { + return ( + + + {text} + + {addSpacer && } + + ); + }; + public render() { const { intl, kibanaVersion, useDefaultContent, kibanaDocLink } = this.props; const { helpExtension } = this.state; @@ -137,6 +234,74 @@ class HeaderHelpMenuUI extends Component { ) : null; + let customContent; + if (helpExtension) { + const { appName, links, content } = helpExtension; + + const getFeedbackText = () => + i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackOnApp', { + defaultMessage: 'Give feedback on {appName}', + values: { appName: helpExtension.appName }, + }); + + const customLinks = + links && + links.map((link, index) => { + const { linkType, title, labels = [], content: text, ...rest } = link; + switch (linkType) { + case 'documentation': + return this.createCustomLink( + index, + , + index < links.length - 1, + { + target: '_blank', + rel: 'noopener', + ...rest, + } + ); + case 'github': + return this.createCustomLink(index, getFeedbackText(), index < links.length - 1, { + iconType: 'logoGithub', + href: this.createGithubUrl(labels, title), + target: '_blank', + rel: 'noopener', + ...rest, + }); + case 'discuss': + return this.createCustomLink(index, getFeedbackText(), index < links.length - 1, { + iconType: 'editorComment', + target: '_blank', + rel: 'noopener', + ...rest, + }); + case 'custom': + return this.createCustomLink(index, text, index < links.length - 1, { ...rest }); + default: + break; + } + }); + + customContent = ( + <> + +

{appName}

+
+ + {customLinks} + {content && ( + <> + {customLinks && } + + + )} + + ); + } + const button = ( { - +

+ +

{
{defaultContent} - {defaultContent && helpExtension && } - {helpExtension && } + {defaultContent && customContent && } + {customContent}
); diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index f9c122b864dce..6d59fc6d9433b 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -19,3 +19,10 @@ export { Header, HeaderProps } from './header'; export { HeaderWrapper } from './header_wrapper'; +export { + ChromeHelpExtensionMenuLink, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, +} from './header_help_menu'; diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 69582f6f1ed52..81b2fdfb0fcc0 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -18,4 +18,12 @@ */ export { LoadingIndicator } from './loading_indicator'; -export { Header, HeaderWrapper } from './header'; +export { + Header, + HeaderWrapper, + ChromeHelpExtensionMenuLink, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, +} from './header'; diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index d159c588718fe..d0374511515d1 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -10,6 +10,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiBasicTable.selectThisRow": "Select this row", "euiBasicTable.tableDescription": [Function], "euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.", + "euiBreadcrumbs.collapsedBadge.ariaLabel": "Show all breadcrumbs", "euiCardSelect.select": "Select", "euiCardSelect.selected": "Selected", "euiCardSelect.unavailable": "Unavailable", @@ -21,6 +22,20 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiCollapsedItemActions.allActions": "All actions", "euiColorPicker.screenReaderAnnouncement": "A popup with a range of selectable colors opened. Tab forward to cycle through colors choices or press escape to close this popup.", "euiColorPicker.swatchAriaLabel": [Function], + "euiColorStopThumb.removeLabel": "Remove this stop", + "euiColorStopThumb.screenReaderAnnouncement": "A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.", + "euiColorStops.screenReaderAnnouncement": [Function], + "euiColumnSelector.hideAll": "Hide all", + "euiColumnSelector.selectAll": "Show all", + "euiColumnSorting.clearAll": "Clear sorting", + "euiColumnSorting.emptySorting": "Currently no fields are sorted", + "euiColumnSorting.pickFields": "Pick fields to sort by", + "euiColumnSorting.sortFieldAriaLabel": "Sort by:", + "euiColumnSortingDraggable.activeSortLabel": "is sorting this data grid", + "euiColumnSortingDraggable.defaultSortAsc": "A-Z", + "euiColumnSortingDraggable.defaultSortDesc": "Z-A", + "euiColumnSortingDraggable.removeSortLabel": "Remove from data grid sort:", + "euiColumnSortingDraggable.toggleLegend": "Select sorting method for field:", "euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options", "euiComboBoxOptionsList.alreadyAdded": [Function], "euiComboBoxOptionsList.createCustomOption": [Function], @@ -28,6 +43,19 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available", "euiComboBoxOptionsList.noMatchingOptions": [Function], "euiComboBoxPill.removeSelection": [Function], + "euiCommonlyUsedTimeRanges.legend": "Commonly used", + "euiDataGrid.screenReaderNotice": "Cell contains interactive content.", + "euiDataGridCell.expandButtonTitle": "Click or hit enter to interact with cell content", + "euiDataGridSchema.booleanSortTextAsc": "True-False", + "euiDataGridSchema.booleanSortTextDesc": "False-True", + "euiDataGridSchema.currencySortTextAsc": "Low-High", + "euiDataGridSchema.currencySortTextDesc": "High-Low", + "euiDataGridSchema.dateSortTextAsc": "New-Old", + "euiDataGridSchema.dateSortTextDesc": "Old-New", + "euiDataGridSchema.jsonSortTextAsc": "Small-Large", + "euiDataGridSchema.jsonSortTextDesc": "Large-Small", + "euiDataGridSchema.numberSortTextAsc": "Low-High", + "euiDataGridSchema.numberSortTextDesc": "High-Low", "euiFilterButton.filterBadge": [Function], "euiForm.addressFormErrors": "Please address the errors in your form.", "euiFormControlLayoutClearButton.label": "Clear input", @@ -35,25 +63,45 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiHeaderLinks.appNavigation": "App navigation", "euiHeaderLinks.openNavigationMenu": "Open navigation menu", "euiHue.label": "Select the HSV color mode \\"hue\\" value", + "euiImage.closeImage": [Function], + "euiImage.openImage": [Function], + "euiLink.external.ariaLabel": "External link", "euiModal.closeModal": "Closes this modal window", "euiPagination.jumpToLastPage": [Function], "euiPagination.nextPage": "Next page", "euiPagination.pageOfTotal": [Function], "euiPagination.previousPage": "Previous page", - "euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.", + "euiPopover.screenReaderAnnouncement": "You are in a dialog. To close this dialog, hit escape.", + "euiQuickSelect.applyButton": "Apply", + "euiQuickSelect.fullDescription": [Function], + "euiQuickSelect.legendText": "Quick select a time range", + "euiQuickSelect.nextLabel": "Next time window", + "euiQuickSelect.previousLabel": "Previous time window", + "euiQuickSelect.quickSelectTitle": "Quick select", + "euiQuickSelect.tenseLabel": "Time tense", + "euiQuickSelect.unitLabel": "Time unit", + "euiQuickSelect.valueLabel": "Time value", + "euiRefreshInterval.fullDescription": [Function], + "euiRefreshInterval.legend": "Refresh every", + "euiRefreshInterval.start": "Start", + "euiRefreshInterval.stop": "Stop", + "euiRelativeTab.fullDescription": [Function], + "euiRelativeTab.relativeDate": [Function], + "euiRelativeTab.roundingLabel": [Function], + "euiRelativeTab.unitInputLabel": "Relative time span", "euiSaturation.roleDescription": "HSV color mode saturation and value selection", "euiSaturation.screenReaderAnnouncement": "Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode \\"saturation\\" and \\"value\\" numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the \\"saturation\\" value. Up and down decrease and increase (respectively) the \\"value\\" value.", "euiSelectable.loadingOptions": "Loading options", "euiSelectable.noAvailableOptions": "There aren't any options available", "euiSelectable.noMatchingOptions": [Function], "euiStat.loadingText": "Statistic is loading", - "euiStep.completeStep": "Step", - "euiStep.incompleteStep": "Incomplete Step", + "euiStep.ariaLabel": [Function], "euiStepHorizontal.buttonTitle": [Function], "euiStepHorizontal.step": "Step", "euiStepNumber.hasErrors": "has errors", "euiStepNumber.hasWarnings": "has warnings", "euiStepNumber.isComplete": "complete", + "euiStyleSelector.buttonText": "Density", "euiSuperDatePicker.showDatesButtonLabel": "Show dates", "euiSuperSelect.screenReaderAnnouncement": [Function], "euiSuperSelectControl.selectAnOption": [Function], @@ -68,6 +116,8 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", + "euiTreeView.ariaLabel": [Function], + "euiTreeView.listNavigationInstructions": "You can quickly navigate this list using arrow keys.", }, } } diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 17cdf56cd43c0..721c5d49634f4 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -65,6 +65,13 @@ export class I18nService { 'Screen reader announcement that functionality is available in the page document', } ), + 'euiBreadcrumbs.collapsedBadge.ariaLabel': i18n.translate( + 'core.euiBreadcrumbs.collapsedBadge.ariaLabel', + { + defaultMessage: 'Show all breadcrumbs', + description: 'Displayed when one or more breadcrumbs are hidden.', + } + ), 'euiCardSelect.select': i18n.translate('core.euiCardSelect.select', { defaultMessage: 'Select', description: 'Displayed button text when a card option can be selected.', @@ -117,6 +124,80 @@ export class I18nService { description: 'Screen reader text to describe the action and hex value of the selectable option', }), + 'euiColorStopThumb.removeLabel': i18n.translate('core.euiColorStopThumb.removeLabel', { + defaultMessage: 'Remove this stop', + description: 'Label accompanying a button whose action will remove the color stop', + }), + 'euiColorStopThumb.screenReaderAnnouncement': i18n.translate( + 'core.euiColorStopThumb.screenReaderAnnouncement', + { + defaultMessage: + 'A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.', + description: + 'Message when the color picker popover has opened for an individual color stop thumb.', + } + ), + 'euiColorStops.screenReaderAnnouncement': ({ label, readOnly, disabled }: EuiValues) => + i18n.translate('core.euiColorStops.screenReaderAnnouncement', { + defaultMessage: + '{label}: {readOnly} {disabled} Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop.', + values: { label, readOnly, disabled }, + description: + 'Screen reader text to describe the composite behavior of the color stops component.', + }), + 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { + defaultMessage: 'Hide all', + }), + 'euiColumnSelector.selectAll': i18n.translate('core.euiColumnSelector.selectAll', { + defaultMessage: 'Show all', + }), + 'euiColumnSorting.clearAll': i18n.translate('core.euiColumnSorting.clearAll', { + defaultMessage: 'Clear sorting', + }), + 'euiColumnSorting.emptySorting': i18n.translate('core.euiColumnSorting.emptySorting', { + defaultMessage: 'Currently no fields are sorted', + }), + 'euiColumnSorting.pickFields': i18n.translate('core.euiColumnSorting.pickFields', { + defaultMessage: 'Pick fields to sort by', + }), + 'euiColumnSorting.sortFieldAriaLabel': i18n.translate( + 'core.euiColumnSorting.sortFieldAriaLabel', + { + defaultMessage: 'Sort by:', + } + ), + 'euiColumnSortingDraggable.activeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.activeSortLabel', + { + defaultMessage: 'is sorting this data grid', + } + ), + 'euiColumnSortingDraggable.defaultSortAsc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortAsc', + { + defaultMessage: 'A-Z', + description: 'Ascending sort label', + } + ), + 'euiColumnSortingDraggable.defaultSortDesc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortDesc', + { + defaultMessage: 'Z-A', + description: 'Descending sort label', + } + ), + 'euiColumnSortingDraggable.removeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.removeSortLabel', + { + defaultMessage: 'Remove from data grid sort:', + } + ), + 'euiColumnSortingDraggable.toggleLegend': i18n.translate( + 'core.euiColumnSortingDraggable.toggleLegend', + { + defaultMessage: 'Select sorting method for field:', + } + ), 'euiComboBoxOptionsList.allOptionsSelected': i18n.translate( 'core.euiComboBoxOptionsList.allOptionsSelected', { @@ -163,6 +244,88 @@ export class I18nService { values: { children }, description: 'ARIA label, `children` is the human-friendly value of an option', }), + 'euiCommonlyUsedTimeRanges.legend': i18n.translate('core.euiCommonlyUsedTimeRanges.legend', { + defaultMessage: 'Commonly used', + }), + 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { + defaultMessage: 'Cell contains interactive content.', + }), + 'euiDataGridCell.expandButtonTitle': i18n.translate( + 'core.euiDataGridCell.expandButtonTitle', + { + defaultMessage: 'Click or hit enter to interact with cell content', + } + ), + 'euiDataGridSchema.booleanSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextAsc', + { + defaultMessage: 'True-False', + description: 'Ascending boolean label', + } + ), + 'euiDataGridSchema.booleanSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextDesc', + { + defaultMessage: 'False-True', + description: 'Descending boolean label', + } + ), + 'euiDataGridSchema.currencySortTextAsc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending currency label', + } + ), + 'euiDataGridSchema.currencySortTextDesc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending currency label', + } + ), + 'euiDataGridSchema.dateSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextAsc', + { + defaultMessage: 'New-Old', + description: 'Ascending date label', + } + ), + 'euiDataGridSchema.dateSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextDesc', + { + defaultMessage: 'Old-New', + description: 'Descending date label', + } + ), + 'euiDataGridSchema.numberSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending number label', + } + ), + 'euiDataGridSchema.numberSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending number label', + } + ), + 'euiDataGridSchema.jsonSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextAsc', + { + defaultMessage: 'Small-Large', + description: 'Ascending size label', + } + ), + 'euiDataGridSchema.jsonSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextDesc', + { + defaultMessage: 'Large-Small', + description: 'Descending size label', + } + ), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { defaultMessage: '${count} ${filterCountLabel} filters', @@ -195,6 +358,19 @@ export class I18nService { 'euiHue.label': i18n.translate('core.euiHue.label', { defaultMessage: 'Select the HSV color mode "hue" value', }), + 'euiImage.closeImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.closeImage', { + defaultMessage: 'Close full screen {alt} image', + values: { alt }, + }), + 'euiImage.openImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.openImage', { + defaultMessage: 'Open full screen {alt} image', + values: { alt }, + }), + 'euiLink.external.ariaLabel': i18n.translate('core.euiLink.external.ariaLabel', { + defaultMessage: 'External link', + }), 'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', { defaultMessage: 'Closes this modal window', }), @@ -217,9 +393,70 @@ export class I18nService { 'euiPopover.screenReaderAnnouncement': i18n.translate( 'core.euiPopover.screenReaderAnnouncement', { - defaultMessage: 'You are in a popup. To exit this popup, hit Escape.', + defaultMessage: 'You are in a dialog. To close this dialog, hit escape.', } ), + 'euiQuickSelect.applyButton': i18n.translate('core.euiQuickSelect.applyButton', { + defaultMessage: 'Apply', + }), + 'euiQuickSelect.fullDescription': ({ timeTense, timeValue, timeUnit }: EuiValues) => + i18n.translate('core.euiQuickSelect.fullDescription', { + defaultMessage: 'Currently set to {timeTense} {timeValue} {timeUnit}.', + values: { timeTense, timeValue, timeUnit }, + }), + 'euiQuickSelect.legendText': i18n.translate('core.euiQuickSelect.legendText', { + defaultMessage: 'Quick select a time range', + }), + 'euiQuickSelect.nextLabel': i18n.translate('core.euiQuickSelect.nextLabel', { + defaultMessage: 'Next time window', + }), + 'euiQuickSelect.previousLabel': i18n.translate('core.euiQuickSelect.previousLabel', { + defaultMessage: 'Previous time window', + }), + 'euiQuickSelect.quickSelectTitle': i18n.translate('core.euiQuickSelect.quickSelectTitle', { + defaultMessage: 'Quick select', + }), + 'euiQuickSelect.tenseLabel': i18n.translate('core.euiQuickSelect.tenseLabel', { + defaultMessage: 'Time tense', + }), + 'euiQuickSelect.unitLabel': i18n.translate('core.euiQuickSelect.unitLabel', { + defaultMessage: 'Time unit', + }), + 'euiQuickSelect.valueLabel': i18n.translate('core.euiQuickSelect.valueLabel', { + defaultMessage: 'Time value', + }), + 'euiRefreshInterval.fullDescription': ({ optionValue, optionText }: EuiValues) => + i18n.translate('core.euiRefreshInterval.fullDescription', { + defaultMessage: 'Currently set to {optionValue} {optionText}.', + values: { optionValue, optionText }, + }), + 'euiRefreshInterval.legend': i18n.translate('core.euiRefreshInterval.legend', { + defaultMessage: 'Refresh every', + }), + 'euiRefreshInterval.start': i18n.translate('core.euiRefreshInterval.start', { + defaultMessage: 'Start', + }), + 'euiRefreshInterval.stop': i18n.translate('core.euiRefreshInterval.stop', { + defaultMessage: 'Stop', + }), + 'euiRelativeTab.fullDescription': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.fullDescription', { + defaultMessage: 'The unit is changeable. Currently set to {unit}.', + values: { unit }, + }), + 'euiRelativeTab.relativeDate': ({ position }: EuiValues) => + i18n.translate('core.euiRelativeTab.relativeDate', { + defaultMessage: '{position} date', + values: { position }, + }), + 'euiRelativeTab.roundingLabel': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.roundingLabel', { + defaultMessage: 'Round to the {unit}', + values: { unit }, + }), + 'euiRelativeTab.unitInputLabel': i18n.translate('core.euiRelativeTab.unitInputLabel', { + defaultMessage: 'Relative time span', + }), 'euiSaturation.roleDescription': i18n.translate('core.euiSaturation.roleDescription', { defaultMessage: 'HSV color mode saturation and value selection', }), @@ -247,22 +484,18 @@ export class I18nService { 'euiStat.loadingText': i18n.translate('core.euiStat.loadingText', { defaultMessage: 'Statistic is loading', }), - 'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', { - defaultMessage: 'Step', - description: - 'See https://elastic.github.io/eui/#/navigation/steps to know how Step control looks like', - }), - 'euiStep.incompleteStep': i18n.translate('core.euiStep.incompleteStep', { - defaultMessage: 'Incomplete Step', - }), + 'euiStep.ariaLabel': ({ status }: EuiValues) => + i18n.translate('core.euiStep.ariaLabel', { + defaultMessage: '{stepStatus}', + values: { stepStatus: status === 'incomplete' ? 'Incomplete Step' : 'Step' }, + }), 'euiStepHorizontal.buttonTitle': ({ step, title, disabled, isComplete }: EuiValues) => { return i18n.translate('core.euiStepHorizontal.buttonTitle', { - defaultMessage: - 'Step {step}: {title}{titleAppendix, select, completed { is completed} disabled { is disabled} other {}}', + defaultMessage: 'Step {step}: {title}{titleAppendix}', values: { step, title, - titleAppendix: disabled ? 'disabled' : isComplete ? 'completed' : '', + titleAppendix: disabled ? ' is disabled' : isComplete ? ' is complete' : '', }, }); }, @@ -285,6 +518,9 @@ export class I18nService { description: 'Used as the title attribute on an image or svg icon to indicate a given process step is complete', }), + 'euiStyleSelector.buttonText': i18n.translate('core.euiStyleSelector.buttonText', { + defaultMessage: 'Density', + }), 'euiSuperDatePicker.showDatesButtonLabel': i18n.translate( 'core.euiSuperDatePicker.showDatesButtonLabel', { @@ -362,6 +598,17 @@ export class I18nService { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), + 'euiTreeView.ariaLabel': ({ nodeLabel, ariaLabel }: EuiValues) => + i18n.translate('core.euiTreeView.ariaLabel', { + defaultMessage: '{nodeLabel} child of {ariaLabel}', + values: { nodeLabel, ariaLabel }, + }), + 'euiTreeView.listNavigationInstructions': i18n.translate( + 'core.euiTreeView.listNavigationInstructions', + { + defaultMessage: 'You can quickly navigate this list using arrow keys.', + } + ), }; return { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index e040b29814900..c723c282a7caa 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -40,6 +40,11 @@ import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, + ChromeHelpExtensionMenuLink, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, ChromeNavControl, ChromeNavControls, ChromeNavLink, @@ -242,6 +247,11 @@ export { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, + ChromeHelpExtensionMenuLink, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, ChromeNavControl, ChromeNavControls, ChromeNavLink, diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts index dac9d5cea3565..cebd0f017de69 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/src/core/public/injected_metadata/index.ts @@ -22,5 +22,6 @@ export { InjectedMetadataParams, InjectedMetadataSetup, InjectedMetadataStart, + InjectedPluginMetadata, LegacyNavLink, } from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 1110097c1c92b..cf4b72114d5ac 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -69,7 +69,7 @@ describe('setup.getPlugins()', () => { const injectedMetadata = new InjectedMetadataService({ injectedMetadata: { uiPlugins: [ - { id: 'plugin-1', plugin: {} }, + { id: 'plugin-1', plugin: {}, config: { clientProp: 'clientValue' } }, { id: 'plugin-2', plugin: {} }, ], }, @@ -77,7 +77,7 @@ describe('setup.getPlugins()', () => { const plugins = injectedMetadata.setup().getPlugins(); expect(plugins).toEqual([ - { id: 'plugin-1', plugin: {} }, + { id: 'plugin-1', plugin: {}, config: { clientProp: 'clientValue' } }, { id: 'plugin-2', plugin: {} }, ]); }); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index a5342aaa48b72..002f83d9feac4 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -38,6 +38,14 @@ export interface LegacyNavLink { euiIconType?: string; } +export interface InjectedPluginMetadata { + id: PluginName; + plugin: DiscoveredPlugin; + config?: { + [key: string]: unknown; + }; +} + /** @internal */ export interface InjectedMetadataParams { injectedMetadata: { @@ -55,10 +63,7 @@ export interface InjectedMetadataParams { mode: Readonly; packageInfo: Readonly; }; - uiPlugins: Array<{ - id: PluginName; - plugin: DiscoveredPlugin; - }>; + uiPlugins: InjectedPluginMetadata[]; capabilities: Capabilities; legacyMode: boolean; legacyMetadata: { @@ -165,10 +170,7 @@ export interface InjectedMetadataSetup { /** * An array of frontend plugins in topological order. */ - getPlugins: () => Array<{ - id: string; - plugin: DiscoveredPlugin; - }>; + getPlugins: () => InjectedPluginMetadata[]; /** Indicates whether or not we are rendering a known legacy app. */ getLegacyMode: () => boolean; getLegacyMetadata: () => { diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index afd0825ec986c..695f0454f8b65 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -92,6 +92,9 @@ function pluginInitializerContextMock() { dist: false, }, }, + config: { + get: () => ({} as T), + }, }; return mock; diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 94c11f0185427..626c91b6a9668 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -74,4 +74,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index eae45654fce18..f77ddd8f2f696 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -18,7 +18,6 @@ */ import { omit } from 'lodash'; - import { DiscoveredPlugin } from '../../server'; import { PluginOpaqueId, PackageInfo, EnvironmentMode } from '../../server/types'; import { CoreContext } from '../core_system'; @@ -31,7 +30,7 @@ import { CoreSetup, CoreStart } from '../'; * * @public */ -export interface PluginInitializerContext { +export interface PluginInitializerContext { /** * A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. */ @@ -40,6 +39,9 @@ export interface PluginInitializerContext { mode: Readonly; packageInfo: Readonly; }; + readonly config: { + get: () => T; + }; } /** @@ -47,17 +49,27 @@ export interface PluginInitializerContext { * empty but should provide static services in the future, such as config and logging. * * @param coreContext - * @param pluginManinfest + * @param opaqueId + * @param pluginManifest + * @param pluginConfig * @internal */ export function createPluginInitializerContext( coreContext: CoreContext, opaqueId: PluginOpaqueId, - pluginManifest: DiscoveredPlugin + pluginManifest: DiscoveredPlugin, + pluginConfig: { + [key: string]: unknown; + } ): PluginInitializerContext { return { opaqueId, env: coreContext.env, + config: { + get() { + return (pluginConfig as unknown) as T; + }, + }, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 0d8887774e900..2983d7583cb49 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -25,13 +25,14 @@ import { mockPluginInitializerProvider, } from './plugins_service.test.mocks'; -import { PluginName, DiscoveredPlugin } from 'src/core/server'; +import { PluginName } from 'src/core/server'; import { coreMock } from '../mocks'; import { PluginsService, PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; +import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; import { i18nServiceMock } from '../i18n/i18n_service.mock'; @@ -41,7 +42,7 @@ import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.moc import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { CoreSetup, CoreStart } from '..'; +import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; @@ -52,7 +53,7 @@ mockPluginInitializerProvider.mockImplementation( pluginName => mockPluginInitializers.get(pluginName)! ); -let plugins: Array<{ id: string; plugin: DiscoveredPlugin }>; +let plugins: InjectedPluginMetadata[]; type DeeplyMocked = { [P in keyof T]: jest.Mocked }; @@ -62,83 +63,6 @@ let mockSetupContext: DeeplyMocked; let mockStartDeps: DeeplyMocked; let mockStartContext: DeeplyMocked; -beforeEach(() => { - plugins = [ - { id: 'pluginA', plugin: createManifest('pluginA') }, - { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, - { - id: 'pluginC', - plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), - }, - ]; - mockSetupDeps = { - application: applicationServiceMock.createInternalSetupContract(), - context: contextServiceMock.createSetupContract(), - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), - notifications: notificationServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), - }; - mockSetupContext = { - ...mockSetupDeps, - application: expect.any(Object), - }; - mockStartDeps = { - application: applicationServiceMock.createInternalStartContract(), - docLinks: docLinksServiceMock.createStartContract(), - http: httpServiceMock.createStartContract(), - chrome: chromeServiceMock.createStartContract(), - i18n: i18nServiceMock.createStartContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), - notifications: notificationServiceMock.createStartContract(), - overlays: overlayServiceMock.createStartContract(), - uiSettings: uiSettingsServiceMock.createStartContract(), - savedObjects: savedObjectsMock.createStartContract(), - }; - mockStartContext = { - ...mockStartDeps, - application: expect.any(Object), - chrome: omit(mockStartDeps.chrome, 'getComponent'), - }; - - // Reset these for each test. - mockPluginInitializers = new Map(([ - [ - 'pluginA', - jest.fn(() => ({ - setup: jest.fn(() => ({ setupValue: 1 })), - start: jest.fn(() => ({ startValue: 2 })), - stop: jest.fn(), - })), - ], - [ - 'pluginB', - jest.fn(() => ({ - setup: jest.fn((core, deps: any) => ({ - pluginAPlusB: deps.pluginA.setupValue + 1, - })), - start: jest.fn((core, deps: any) => ({ - pluginAPlusB: deps.pluginA.startValue + 1, - })), - stop: jest.fn(), - })), - ], - [ - 'pluginC', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - })), - ], - ] as unknown) as [[PluginName, any]]); -}); - -afterEach(() => { - mockLoadPluginBundle.mockClear(); -}); - function createManifest( id: string, { required = [], optional = [] }: { required?: string[]; optional?: string[]; ui?: boolean } = {} @@ -152,9 +76,88 @@ function createManifest( }; } -test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` +describe('PluginsService', () => { + beforeEach(() => { + plugins = [ + { id: 'pluginA', plugin: createManifest('pluginA') }, + { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, + { + id: 'pluginC', + plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), + }, + ]; + mockSetupDeps = { + application: applicationServiceMock.createInternalSetupContract(), + context: contextServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + http: httpServiceMock.createSetupContract(), + injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + notifications: notificationServiceMock.createSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), + }; + mockSetupContext = { + ...mockSetupDeps, + application: expect.any(Object), + }; + mockStartDeps = { + application: applicationServiceMock.createInternalStartContract(), + docLinks: docLinksServiceMock.createStartContract(), + http: httpServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), + i18n: i18nServiceMock.createStartContract(), + injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + notifications: notificationServiceMock.createStartContract(), + overlays: overlayServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), + }; + mockStartContext = { + ...mockStartDeps, + application: expect.any(Object), + chrome: omit(mockStartDeps.chrome, 'getComponent'), + }; + + // Reset these for each test. + mockPluginInitializers = new Map(([ + [ + 'pluginA', + jest.fn(() => ({ + setup: jest.fn(() => ({ setupValue: 1 })), + start: jest.fn(() => ({ startValue: 2 })), + stop: jest.fn(), + })), + ], + [ + 'pluginB', + jest.fn(() => ({ + setup: jest.fn((core, deps: any) => ({ + pluginAPlusB: deps.pluginA.setupValue + 1, + })), + start: jest.fn((core, deps: any) => ({ + pluginAPlusB: deps.pluginA.startValue + 1, + })), + stop: jest.fn(), + })), + ], + [ + 'pluginC', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + })), + ], + ] as unknown) as [[PluginName, any]]); + }); + + afterEach(() => { + mockLoadPluginBundle.mockClear(); + }); + + describe('#getOpaqueIds()', () => { + it('returns dependency tree of symbols', () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` Map { Symbol(pluginA) => Array [], Symbol(pluginB) => Array [ @@ -165,152 +168,184 @@ test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => { ], } `); -}); - -test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => { - mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); -}); - -test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => { - mockPluginInitializers.set('pluginA', (() => ({})) as any); - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` - ); -}); - -test('`PluginsService.setup` calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginA'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginB'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginC'); -}); - -test('`PluginsService.setup` initalizes plugins with PluginIntitializerContext', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object)); - expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object)); - expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); -}); - -test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - - expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); - expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, { - pluginA: { setupValue: 1 }, - }); - // Does not supply value for `nonexist` optional dep - expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, { - pluginA: { setupValue: 1 }, + }); }); -}); - -test('`PluginsService.setup` does not set missing dependent setup contracts', async () => { - plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set( - 'pluginD', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any - ); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - // If a dependency is missing it should not be in the deps at all, not even as undefined. - const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; - expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); - const pluginDDeps = pluginDInstance.setup.mock.calls[0][1]; - expect(pluginDDeps).not.toHaveProperty('missing'); -}); -test('`PluginsService.setup` returns plugin setup contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - const { contracts } = await pluginsService.setup(mockSetupDeps); - - // Verify that plugin contracts were available - expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); - expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); -}); - -test('`PluginsService.start` exposes dependent start contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - await pluginsService.start(mockStartDeps); - - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - - expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {}); - expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, { - pluginA: { startValue: 2 }, - }); - // Does not supply value for `nonexist` optional dep - expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, { - pluginA: { startValue: 2 }, + describe('#setup()', () => { + it('fails if any bundle cannot be loaded', async () => { + mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not load bundle"` + ); + }); + + it('fails if any plugin instance does not have a setup function', async () => { + mockPluginInitializers.set('pluginA', (() => ({})) as any); + const pluginsService = new PluginsService(mockCoreContext, plugins); + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` + ); + }); + + it('calls loadPluginBundles with http and plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginA' + ); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginB' + ); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginC' + ); + }); + + it('initializes plugins with PluginInitializerContext', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('initializes plugins with associated client configuration', async () => { + const pluginConfig = { + clientProperty: 'some value', + }; + plugins[0].config = pluginConfig; + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + const initializerContext = mockPluginInitializers.get('pluginA')!.mock + .calls[0][0] as PluginInitializerContext; + const config = initializerContext.config.get(); + expect(config).toMatchObject(pluginConfig); + }); + + it('exposes dependent setup contracts to plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + + expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); + expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, + }); + // Does not supply value for `nonexist` optional dep + expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, + }); + }); + + it('does not set missing dependent setup contracts', async () => { + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + // If a dependency is missing it should not be in the deps at all, not even as undefined. + const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; + expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); + const pluginDDeps = pluginDInstance.setup.mock.calls[0][1]; + expect(pluginDDeps).not.toHaveProperty('missing'); + }); + + it('returns plugin setup contracts', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + const { contracts } = await pluginsService.setup(mockSetupDeps); + + // Verify that plugin contracts were available + expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); + expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); + }); }); -}); -test('`PluginsService.start` does not set missing dependent start contracts', async () => { - plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set( - 'pluginD', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any - ); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - await pluginsService.start(mockStartDeps); - - // If a dependency is missing it should not be in the deps at all, not even as undefined. - const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; - expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {}); - const pluginDDeps = pluginDInstance.start.mock.calls[0][1]; - expect(pluginDDeps).not.toHaveProperty('missing'); -}); - -test('`PluginsService.start` returns plugin start contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - const { contracts } = await pluginsService.start(mockStartDeps); - - // Verify that plugin contracts were available - expect((contracts.get('pluginA')! as any).startValue).toEqual(2); - expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); -}); + describe('#start()', () => { + it('exposes dependent start contracts to plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + + expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); + // Does not supply value for `nonexist` optional dep + expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); + }); + + it('does not set missing dependent start contracts', async () => { + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + // If a dependency is missing it should not be in the deps at all, not even as undefined. + const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; + expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + const pluginDDeps = pluginDInstance.start.mock.calls[0][1]; + expect(pluginDDeps).not.toHaveProperty('missing'); + }); + + it('returns plugin start contracts', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + const { contracts } = await pluginsService.start(mockStartDeps); + + // Verify that plugin contracts were available + expect((contracts.get('pluginA')! as any).startValue).toEqual(2); + expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); + }); + }); -test('`PluginService.stop` calls the stop function on each plugin', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); + describe('#stop()', () => { + it('calls the stop function on each plugin', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - await pluginsService.stop(); + await pluginsService.stop(); - expect(pluginAInstance.stop).toHaveBeenCalled(); - expect(pluginBInstance.stop).toHaveBeenCalled(); - expect(pluginCInstance.stop).toHaveBeenCalled(); + expect(pluginAInstance.stop).toHaveBeenCalled(); + expect(pluginBInstance.stop).toHaveBeenCalled(); + expect(pluginCInstance.stop).toHaveBeenCalled(); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 1ab9d7f2fa9b2..c1939a3397647 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { DiscoveredPlugin, PluginName, PluginOpaqueId } from '../../server'; +import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; import { PluginWrapper } from './plugin'; @@ -27,6 +27,7 @@ import { createPluginStartContext, } from './plugin_context'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; +import { InjectedPluginMetadata } from '../injected_metadata'; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; @@ -55,15 +56,12 @@ export class PluginsService implements CoreService - ) { + constructor(private readonly coreContext: CoreContext, plugins: InjectedPluginMetadata[]) { // Generate opaque ids const opaqueIds = new Map(plugins.map(p => [p.id, Symbol(p.id)])); // Setup dependency map and plugin wrappers - plugins.forEach(({ id, plugin }) => { + plugins.forEach(({ id, plugin, config = {} }) => { // Setup map of dependencies this.pluginDependencies.set(id, [ ...plugin.requiredPlugins, @@ -76,7 +74,7 @@ export class PluginsService implements CoreService () => void; +export interface ChromeHelpExtension { + appName: string; + content?: (element: HTMLDivElement) => () => void; + links?: ChromeHelpExtensionMenuLink[]; +} + +// @public (undocumented) +export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { + linkType: 'custom'; + content: React.ReactNode; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { + linkType: 'discuss'; + href: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { + linkType: 'documentation'; + href: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { + linkType: 'github'; + labels: string[]; + title?: string; +}; + +// @public (undocumented) +export type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; // @public (undocumented) export interface ChromeNavControl { @@ -695,7 +729,11 @@ export interface Plugin = (core: PluginInitializerContext) => Plugin; // @public -export interface PluginInitializerContext { +export interface PluginInitializerContext { + // (undocumented) + readonly config: { + get: () => T; + }; // (undocumented) readonly env: { mode: Readonly; @@ -808,7 +846,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts index 0de10e8fb4f77..275bda17ab92f 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -18,17 +18,17 @@ */ import * as legacyElasticsearch from 'elasticsearch'; -import { retryCallCluster } from './retry_call_cluster'; +import { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster'; +import { loggingServiceMock } from '../logging/logging_service.mock'; describe('retryCallCluster', () => { - it('retries ES API calls that rejects with NoConnection errors', () => { + it('retries ES API calls that rejects with NoConnections', () => { expect.assertions(1); const callEsApi = jest.fn(); let i = 0; + const ErrorConstructor = legacyElasticsearch.errors.NoConnections; callEsApi.mockImplementation(() => { - return i++ <= 2 - ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) - : Promise.resolve('success'); + return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); }); const retried = retryCallCluster(callEsApi); return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); @@ -57,3 +57,77 @@ describe('retryCallCluster', () => { return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); }); }); + +describe('migrationsRetryCallCluster', () => { + const errors = [ + 'NoConnections', + 'ConnectionFault', + 'ServiceUnavailable', + 'RequestTimeout', + 'AuthenticationException', + 'AuthorizationException', + ]; + + const mockLogger = loggingServiceMock.create(); + + beforeEach(() => { + loggingServiceMock.clear(mockLogger); + }); + + errors.forEach(errorName => { + it('retries ES API calls that rejects with ' + errorName, () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + const ErrorConstructor = (legacyElasticsearch.errors as any)[errorName]; + callEsApi.mockImplementation(() => { + return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + }); + + it('rejects when ES API calls reject with other errors', async () => { + expect.assertions(3); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + i++; + + return i === 1 + ? Promise.reject(new Error('unknown error')) + : i === 2 + ? Promise.resolve('success') + : i === 3 || i === 4 + ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) + : i === 5 + ? Promise.reject(new Error('unknown error')) + : null; + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); + + it('logs only once for each unique error message', async () => { + const callEsApi = jest.fn(); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.AuthenticationException()); + callEsApi.mockResolvedValueOnce('done'); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + await retried('endpoint'); + expect(loggingServiceMock.collect(mockLogger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unable to connect to Elasticsearch. Error: No Living connections", + ], + Array [ + "Unable to connect to Elasticsearch. Error: Authentication Exception", + ], + ] + `); + }); +}); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts index 2e4afa682ea75..89d7b88b1675a 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -22,6 +22,62 @@ import { defer, throwError, iif, timer } from 'rxjs'; import * as legacyElasticsearch from 'elasticsearch'; import { CallAPIOptions } from '.'; +import { Logger } from '../logging'; + +const esErrors = legacyElasticsearch.errors; + +/** + * Retries the provided Elasticsearch API call when an error such as + * `AuthenticationException` `NoConnections`, `ConnectionFault`, + * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will + * be retried once a second, indefinitely, until a successful response or a + * different error is received. + * + * @param apiCaller + */ + +// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged +export function migrationsRetryCallCluster( + apiCaller: ( + endpoint: string, + clientParams: Record, + options?: CallAPIOptions + ) => Promise, + log: Logger, + delay: number = 2500 +) { + const previousErrors: string[] = []; + return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { + return defer(() => apiCaller(endpoint, clientParams, options)) + .pipe( + retryWhen(error$ => + error$.pipe( + concatMap((error, i) => { + if (!previousErrors.includes(error.message)) { + log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); + previousErrors.push(error.message); + } + return iif( + () => { + return ( + error instanceof esErrors.NoConnections || + error instanceof esErrors.ConnectionFault || + error instanceof esErrors.ServiceUnavailable || + error instanceof esErrors.RequestTimeout || + error instanceof esErrors.AuthenticationException || + error instanceof esErrors.AuthorizationException + ); + }, + timer(delay), + throwError(error) + ); + }) + ) + ) + ) + .toPromise(); + }; +} /** * Retries the provided Elasticsearch API call when a `NoConnections` error is diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 00c9aedc42cfb..e9a2571382edc 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({ put: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), + handleLegacyErrors: jest.fn().mockImplementation(handler => handler), }); const createSetupContractMock = () => { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index ff1ff3acfae3d..2fa67750f6406 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 70d7ae00f917e..481d8e1bbf49b 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -164,6 +164,53 @@ describe('Handler', () => { }); }); +describe('handleLegacyErrors', () => { + it('properly convert Boom errors', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false }, + router.handleLegacyErrors((context, req, res) => { + throw Boom.notFound(); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('returns default error when non-Boom errors are thrown', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { + path: '/', + validate: false, + }, + router.handleLegacyErrors((context, req, res) => { + throw new Error('Unexpected'); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + }); +}); + describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts new file mode 100644 index 0000000000000..aa20b49dc9c91 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.test.ts @@ -0,0 +1,80 @@ +/* + * 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 Boom from 'boom'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; +import { wrapErrors } from './error_wrapper'; +import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server'; + +const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); +}; + +describe('wrapErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = wrapErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBeInstanceOf(KibanaResponse); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = wrapErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); +}); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts new file mode 100644 index 0000000000000..706a9fe3b8887 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.ts @@ -0,0 +1,48 @@ +/* + * 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 Boom from 'boom'; +import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from './request'; +import { KibanaResponseFactory } from './response'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '../../../server'; + +export const wrapErrors =

( + handler: RequestHandler +): RequestHandler => { + return async ( + context: RequestHandlerContext, + request: KibanaRequest, TypeOf, TypeOf>, + response: KibanaResponseFactory + ) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e)) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 56ed9ca11edc1..f07ad3cfe85c0 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter } from './router'; +export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 6b7e2e3ad14cd..a13eae51a19a6 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -27,6 +27,7 @@ import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from '. import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; +import { wrapErrors } from './error_wrapper'; interface RouterRoute { method: RouteMethod; @@ -35,6 +36,15 @@ interface RouterRoute { handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } +/** + * Handler to declare a route. + * @public + */ +export type RouteRegistrar =

( + route: RouteConfig, + handler: RequestHandler +) => void; + /** * Registers route handlers for specified resource path and method. * See {@link RouteConfig} and {@link RequestHandler} for more information about arguments to route registrations. @@ -52,40 +62,36 @@ export interface IRouter { * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - get:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + get: RouteRegistrar; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - post:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + post: RouteRegistrar; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - put:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + put: RouteRegistrar; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - delete:

( - route: RouteConfig, + delete: RouteRegistrar; + + /** + * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + * @param handler {@link RequestHandler} - a route handler to wrap + */ + handleLegacyErrors:

( handler: RequestHandler - ) => void; + ) => RequestHandler; /** * Returns all routes registered with the this router. @@ -188,6 +194,12 @@ export class Router implements IRouter { return [...this.routes]; } + public handleLegacyErrors

( + handler: RequestHandler + ): RequestHandler { + return wrapErrors(handler); + } + private async handle

({ routeSchemas, request, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2a5631ad1c380..31dec2c9b96ff 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -114,6 +114,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, SessionStorage, SessionStorageCookieOptions, SessionStorageFactory, @@ -123,6 +124,8 @@ export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { DiscoveredPlugin, Plugin, + PluginConfigDescriptor, + PluginConfigSchema, PluginInitializer, PluginInitializerContext, PluginManifest, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index e2aefd846d978..030caa8324521 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -41,7 +41,7 @@ import { configServiceMock } from '../config/config_service.mock'; import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; +import { DiscoveredPlugin } from '../plugins'; import { KibanaMigrator } from '../saved_objects/migrations'; import { ISavedObjectsClientProvider } from '../saved_objects'; @@ -84,7 +84,8 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + browserConfigs: new Map(), }, }, }, diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index e457f01a1941c..6aab03a01675d 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -291,12 +291,13 @@ test('`stop` calls `stop` defined by the plugin instance', async () => { describe('#getConfigSchema()', () => { it('reads config schema from plugin', () => { const pluginSchema = schema.any(); + const configDescriptor = { + schema: pluginSchema, + }; jest.doMock( 'plugin-with-schema/server', () => ({ - config: { - schema: pluginSchema, - }, + config: configDescriptor, }), { virtual: true } ); @@ -309,7 +310,7 @@ describe('#getConfigSchema()', () => { initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(pluginSchema); + expect(plugin.getConfigDescriptor()).toBe(configDescriptor); }); it('returns null if config definition not specified', () => { @@ -322,7 +323,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(null); + expect(plugin.getConfigDescriptor()).toBe(null); }); it('returns null for plugins without a server part', () => { @@ -334,7 +335,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(null); + expect(plugin.getConfigDescriptor()).toBe(null); }); it('throws if plugin contains invalid schema', () => { @@ -357,7 +358,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.getConfigDescriptor()).toThrowErrorMatchingInlineSnapshot( `"Configuration schema expected to be an instance of Type"` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index ff61d8033a484..c0b484515ccce 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -27,9 +27,9 @@ import { Plugin, PluginInitializerContext, PluginManifest, - PluginConfigSchema, PluginInitializer, PluginOpaqueId, + PluginConfigDescriptor, } from './types'; import { CoreSetup, CoreStart } from '..'; @@ -128,7 +128,7 @@ export class PluginWrapper< this.instance = undefined; } - public getConfigSchema(): PluginConfigSchema { + public getConfigDescriptor(): PluginConfigDescriptor | null { if (!this.manifest.server) { return null; } @@ -141,10 +141,11 @@ export class PluginWrapper< return null; } - if (!(pluginDefinition.config.schema instanceof Type)) { + const configDescriptor = pluginDefinition.config; + if (!(configDescriptor.schema instanceof Type)) { throw new Error('Configuration schema expected to be an instance of Type'); } - return pluginDefinition.config.schema; + return configDescriptor; } private createPluginInstance() { diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index c8b6bed044fd7..8d3c6a8c909a2 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -30,8 +30,9 @@ const createServiceMock = () => { mocked.setup.mockResolvedValue({ contracts: new Map(), uiPlugins: { - public: new Map(), + browserConfigs: new Map(), internal: new Map(), + public: new Map(), }, }); mocked.start.mockResolvedValue({ contracts: new Map() }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 0b3bc0759463c..7e55faa43360e 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -32,10 +32,13 @@ import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; +import { take } from 'rxjs/operators'; +import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; +let config$: BehaviorSubject; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -90,301 +93,435 @@ const createPlugin = ( }); }; -beforeEach(async () => { - mockPackage.raw = { - branch: 'feature-v1', - version: 'v1', - build: { - distributable: true, - number: 100, - sha: 'feature-v1-build-sha', - }, - }; - - coreId = Symbol('core'); - env = Env.createDefault(getEnvOptions()); - - configService = new ConfigService( - new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), - env, - logger - ); - await configService.setSchema(config.path, config.schema); - pluginsService = new PluginsService({ coreId, env, logger, configService }); - - [mockPluginSystem] = MockPluginsSystem.mock.instances as any; -}); - -afterEach(() => { - jest.clearAllMocks(); -}); +describe('PluginsService', () => { + beforeEach(async () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; -test('`discover` throws if plugin has an invalid manifest', async () => { - mockDiscover.mockReturnValue({ - error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), - plugin$: from([]), - }); + coreId = Symbol('core'); + env = Env.createDefault(getEnvOptions()); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` -[Error: Failed to initialize plugins: - Invalid JSON (invalid-manifest, path-1)] -`); - expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` -Array [ - Array [ - [Error: Invalid JSON (invalid-manifest, path-1)], - ], -] -`); -}); + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ plugins: { initialize: true } }) + ); + configService = new ConfigService(config$, env, logger); + await configService.setSchema(config.path, config.schema); + pluginsService = new PluginsService({ coreId, env, logger, configService }); -test('`discover` throws if plugin required Kibana version is incompatible with the current version', async () => { - mockDiscover.mockReturnValue({ - error$: from([ - PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), - ]), - plugin$: from([]), + [mockPluginSystem] = MockPluginsSystem.mock.instances as any; }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` -[Error: Failed to initialize plugins: - Incompatible version (incompatible-version, path-3)] -`); - expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` -Array [ - Array [ - [Error: Incompatible version (incompatible-version, path-3)], - ], -] -`); -}); - -test('`discover` throws if discovered plugins with conflicting names', async () => { - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('conflicting-id', { - path: 'path-4', - version: 'some-version', - configPath: 'path', - requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], - optionalPlugins: ['some-optional-plugin'], - }), - createPlugin('conflicting-id', { - path: 'path-4', - version: 'some-version', - configPath: 'path', - requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], - optionalPlugins: ['some-optional-plugin'], - }), - ]), + afterEach(() => { + jest.clearAllMocks(); }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( - `[Error: Plugin with id "conflicting-id" is already registered!]` - ); + describe('#discover()', () => { + it('throws if plugin has an invalid manifest', async () => { + mockDiscover.mockReturnValue({ + error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), + plugin$: from([]), + }); + + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + [Error: Failed to initialize plugins: + Invalid JSON (invalid-manifest, path-1)] + `); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Invalid JSON (invalid-manifest, path-1)], + ], + ] + `); + }); + + it('throws if plugin required Kibana version is incompatible with the current version', async () => { + mockDiscover.mockReturnValue({ + error$: from([ + PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), + ]), + plugin$: from([]), + }); + + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + [Error: Failed to initialize plugins: + Incompatible version (incompatible-version, path-3)] + `); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Incompatible version (incompatible-version, path-3)], + ], + ] + `); + }); + + it('throws if discovered plugins with conflicting names', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('conflicting-id', { + path: 'path-4', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + createPlugin('conflicting-id', { + path: 'path-4', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + ]), + }); + + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( + `[Error: Plugin with id "conflicting-id" is already registered!]` + ); + + expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + }); + + it('properly detects plugins that should be disabled.', async () => { + jest + .spyOn(configService, 'isEnabledAtPath') + .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); + + mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('explicitly-disabled-plugin', { + disabled: true, + path: 'path-1', + configPath: 'path-1', + }), + createPlugin('plugin-with-missing-required-deps', { + path: 'path-2', + configPath: 'path-2', + requiredPlugins: ['missing-plugin'], + }), + createPlugin('plugin-with-disabled-transitive-dep', { + path: 'path-3', + configPath: 'path-3', + requiredPlugins: ['another-explicitly-disabled-plugin'], + }), + createPlugin('another-explicitly-disabled-plugin', { + disabled: true, + path: 'path-4', + configPath: 'path-4-disabled', + }), + ]), + }); + + await pluginsService.discover(); + const setup = await pluginsService.setup(setupDeps); + + expect(setup.contracts).toBeInstanceOf(Map); + expect(setup.uiPlugins.public).toBeInstanceOf(Map); + expect(setup.uiPlugins.internal).toBeInstanceOf(Map); + expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); + + expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin \\"explicitly-disabled-plugin\\" is disabled.", + ], + Array [ + "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + ], + Array [ + "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + ], + Array [ + "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", + ], + ] + `); + }); + + it('does not throw in case of mutual plugin dependencies', async () => { + const firstPlugin = createPlugin('first-plugin', { + path: 'path-1', + requiredPlugins: ['second-plugin'], + }); + const secondPlugin = createPlugin('second-plugin', { + path: 'path-2', + requiredPlugins: ['first-plugin'], + }); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); - expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); -}); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([firstPlugin, secondPlugin]), + }); -test('`discover` properly detects plugins that should be disabled.', async () => { - jest - .spyOn(configService, 'isEnabledAtPath') - .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); + await expect(pluginsService.discover()).resolves.toBeUndefined(); - mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + }); - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('explicitly-disabled-plugin', { - disabled: true, + it('does not throw in case of cyclic plugin dependencies', async () => { + const firstPlugin = createPlugin('first-plugin', { path: 'path-1', - configPath: 'path-1', - }), - createPlugin('plugin-with-missing-required-deps', { + requiredPlugins: ['second-plugin'], + }); + const secondPlugin = createPlugin('second-plugin', { path: 'path-2', - configPath: 'path-2', - requiredPlugins: ['missing-plugin'], - }), - createPlugin('plugin-with-disabled-transitive-dep', { + requiredPlugins: ['third-plugin', 'last-plugin'], + }); + const thirdPlugin = createPlugin('third-plugin', { path: 'path-3', - configPath: 'path-3', - requiredPlugins: ['another-explicitly-disabled-plugin'], - }), - createPlugin('another-explicitly-disabled-plugin', { - disabled: true, + requiredPlugins: ['last-plugin', 'first-plugin'], + }); + const lastPlugin = createPlugin('last-plugin', { path: 'path-4', - configPath: 'path-4-disabled', - }), - ]), - }); - - await pluginsService.discover(); - const setup = await pluginsService.setup(setupDeps); - - expect(setup.contracts).toBeInstanceOf(Map); - expect(setup.uiPlugins.public).toBeInstanceOf(Map); - expect(setup.uiPlugins.internal).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); - - expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` -Array [ - Array [ - "Plugin \\"explicitly-disabled-plugin\\" is disabled.", - ], - Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", - ], - Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", - ], - Array [ - "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", - ], -] -`); -}); - -test('`discover` does not throw in case of mutual plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], - }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['first-plugin'], - }); - - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([firstPlugin, secondPlugin]), - }); - - await expect(pluginsService.discover()).resolves.toBeUndefined(); - - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); -}); - -test('`discover` does not throw in case of cyclic plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], - }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['third-plugin', 'last-plugin'], - }); - const thirdPlugin = createPlugin('third-plugin', { - path: 'path-3', - requiredPlugins: ['last-plugin', 'first-plugin'], - }); - const lastPlugin = createPlugin('last-plugin', { - path: 'path-4', - requiredPlugins: ['first-plugin'], - }); - const missingDepsPlugin = createPlugin('missing-deps-plugin', { - path: 'path-5', - requiredPlugins: ['not-a-plugin'], - }); - - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), + requiredPlugins: ['first-plugin'], + }); + const missingDepsPlugin = createPlugin('missing-deps-plugin', { + path: 'path-5', + requiredPlugins: ['not-a-plugin'], + }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), + }); + + await expect(pluginsService.discover()).resolves.toBeUndefined(); + + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin); + }); + + it('properly invokes plugin discovery and ignores non-critical errors.', async () => { + const firstPlugin = createPlugin('some-id', { + path: 'path-1', + configPath: 'path', + requiredPlugins: ['some-other-id'], + optionalPlugins: ['missing-optional-dep'], + }); + const secondPlugin = createPlugin('some-other-id', { + path: 'path-2', + version: 'some-other-version', + configPath: ['plugin', 'path'], + }); + + mockDiscover.mockReturnValue({ + error$: from([ + PluginDiscoveryError.missingManifest('path-2', new Error('No manifest')), + PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')), + PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')), + ]), + plugin$: from([firstPlugin, secondPlugin]), + }); + + await pluginsService.discover(); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockDiscover).toHaveBeenCalledWith( + { + additionalPluginPaths: [], + initialize: true, + pluginSearchPaths: [ + resolve(process.cwd(), 'src', 'plugins'), + resolve(process.cwd(), 'x-pack', 'plugins'), + resolve(process.cwd(), 'plugins'), + resolve(process.cwd(), '..', 'kibana-extra'), + ], + }, + { coreId, env, logger, configService } + ); + + const logs = loggingServiceMock.collect(logger); + expect(logs.info).toHaveLength(0); + expect(logs.error).toHaveLength(0); + }); + + it('registers plugin config schema in config service', async () => { + const configSchema = schema.string(); + jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); + jest.doMock( + join('path-with-schema', 'server'), + () => ({ + config: { + schema: configSchema, + }, + }), + { + virtual: true, + } + ); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('some-id', { + path: 'path-with-schema', + configPath: 'path', + }), + ]), + }); + await pluginsService.discover(); + expect(configService.setSchema).toBeCalledWith('path', configSchema); + }); }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); - - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin); -}); - -test('`discover` properly invokes plugin discovery and ignores non-critical errors.', async () => { - const firstPlugin = createPlugin('some-id', { - path: 'path-1', - configPath: 'path', - requiredPlugins: ['some-other-id'], - optionalPlugins: ['missing-optional-dep'], - }); - const secondPlugin = createPlugin('some-other-id', { - path: 'path-2', - version: 'some-other-version', - configPath: ['plugin', 'path'], + describe('#generateUiPluginsConfigs()', () => { + const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPlugin] => [ + plugin.name, + { + id: plugin.name, + configPath: plugin.manifest.configPath, + requiredPlugins: [], + optionalPlugins: [], + }, + ]; + + it('properly generates client configs for plugins according to `exposeToBrowser`', async () => { + jest.doMock( + join('plugin-with-expose', 'server'), + () => ({ + config: { + exposeToBrowser: { + sharedProp: true, + }, + schema: schema.object({ + serverProp: schema.string({ defaultValue: 'serverProp default value' }), + sharedProp: schema.string({ defaultValue: 'sharedProp default value' }), + }), + }, + }), + { + virtual: true, + } + ); + const plugin = createPlugin('plugin-with-expose', { + path: 'plugin-with-expose', + configPath: 'path', + }); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([plugin]), + }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); + + await pluginsService.discover(); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); + + const uiConfig$ = browserConfigs.get('plugin-with-expose'); + expect(uiConfig$).toBeDefined(); + + const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); + expect(uiConfig).toMatchInlineSnapshot(` + Object { + "sharedProp": "sharedProp default value", + } + `); + }); + + it('does not generate config for plugins not exposing to client', async () => { + jest.doMock( + join('plugin-without-expose', 'server'), + () => ({ + config: { + schema: schema.object({ + serverProp: schema.string({ defaultValue: 'serverProp default value' }), + }), + }, + }), + { + virtual: true, + } + ); + const plugin = createPlugin('plugin-without-expose', { + path: 'plugin-without-expose', + configPath: 'path', + }); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([plugin]), + }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); + + await pluginsService.discover(); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); + + expect([...browserConfigs.entries()]).toHaveLength(0); + }); }); - mockDiscover.mockReturnValue({ - error$: from([ - PluginDiscoveryError.missingManifest('path-2', new Error('No manifest')), - PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')), - PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')), - ]), - plugin$: from([firstPlugin, secondPlugin]), + describe('#setup()', () => { + describe('uiPlugins.internal', () => { + it('includes disabled plugins', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-1', { + path: 'path-1', + version: 'some-version', + configPath: 'plugin1', + }), + createPlugin('plugin-2', { + path: 'path-2', + version: 'some-version', + configPath: 'plugin2', + }), + ]), + }); + + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + config$.next( + new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) + ); + + await pluginsService.discover(); + const { uiPlugins } = await pluginsService.setup({} as any); + expect(uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1" => Object { + "entryPointPath": "path-1/public", + }, + "plugin-2" => Object { + "entryPointPath": "path-2/public", + }, + } + `); + }); + }); }); - await pluginsService.discover(); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); - - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockDiscover).toHaveBeenCalledWith( - { - additionalPluginPaths: [], - initialize: true, - pluginSearchPaths: [ - resolve(process.cwd(), 'src', 'plugins'), - resolve(process.cwd(), 'x-pack', 'plugins'), - resolve(process.cwd(), 'plugins'), - resolve(process.cwd(), '..', 'kibana-extra'), - ], - }, - { coreId, env, logger, configService } - ); - - const logs = loggingServiceMock.collect(logger); - expect(logs.info).toHaveLength(0); - expect(logs.error).toHaveLength(0); -}); - -test('`stop` stops plugins system', async () => { - await pluginsService.stop(); - expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); -}); - -test('`discover` registers plugin config schema in config service', async () => { - const configSchema = schema.string(); - jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); - jest.doMock( - join('path-with-schema', 'server'), - () => ({ - config: { - schema: configSchema, - }, - }), - { - virtual: true, - } - ); - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('some-id', { - path: 'path-with-schema', - configPath: 'path', - }), - ]), + describe('#stop()', () => { + it('`stop` stops plugins system', async () => { + await pluginsService.stop(); + expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + }); }); - await pluginsService.discover(); - expect(configService.setSchema).toBeCalledWith('path', configSchema); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 38fe519567a63..4c73c2a304dc4 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,17 +25,32 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; +import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup } from '../internal_types'; +import { IConfigService } from '../config'; +import { pick } from '../../utils'; /** @public */ export interface PluginsServiceSetup { contracts: Map; uiPlugins: { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ public: Map; - internal: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; }; } @@ -54,11 +69,15 @@ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-e export class PluginsService implements CoreService { private readonly log: Logger; private readonly pluginsSystem: PluginsSystem; + private readonly configService: IConfigService; private readonly config$: Observable; + private readonly pluginConfigDescriptors = new Map(); + private readonly uiPluginInternalInfo = new Map(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); this.pluginsSystem = new PluginsSystem(coreContext); + this.configService = coreContext.configService; this.config$ = coreContext.configService .atPath('plugins') .pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env))); @@ -82,17 +101,21 @@ export class PluginsService implements CoreService(); if (!config.initialize || this.coreContext.env.isDevClusterMaster) { this.log.info('Plugin initialization disabled.'); - return { - contracts: new Map(), - uiPlugins: this.pluginsSystem.uiPlugins(), - }; + } else { + contracts = await this.pluginsSystem.setupPlugins(deps); } + const uiPlugins = this.pluginsSystem.uiPlugins(); return { - contracts: await this.pluginsSystem.setupPlugins(deps), - uiPlugins: this.pluginsSystem.uiPlugins(), + contracts, + uiPlugins: { + internal: this.uiPluginInternalInfo, + public: uiPlugins, + browserConfigs: this.generateUiPluginsConfigs(uiPlugins), + }, }; } @@ -107,6 +130,38 @@ export class PluginsService implements CoreService + ): Map> { + return new Map( + [...uiPlugins] + .filter(([pluginId, _]) => { + const configDescriptor = this.pluginConfigDescriptors.get(pluginId); + return ( + configDescriptor && + configDescriptor.exposeToBrowser && + Object.values(configDescriptor?.exposeToBrowser).some(exposed => exposed) + ); + }) + .map(([pluginId, plugin]) => { + const configDescriptor = this.pluginConfigDescriptors.get(pluginId)!; + return [ + pluginId, + this.configService.atPath(plugin.configPath).pipe( + map((config: any) => + pick( + config || {}, + Object.entries(configDescriptor.exposeToBrowser!) + .filter(([_, exposed]) => exposed) + .map(([key, _]) => key) + ) + ) + ), + ]; + }) + ); + } + private async handleDiscoveryErrors(error$: Observable) { // At this stage we report only errors that can occur when new platform plugin // manifest is present, otherwise we can't be sure that the plugin is for the new @@ -138,9 +193,13 @@ export class PluginsService implements CoreService { - const schema = plugin.getConfigSchema(); - if (schema) { - await this.coreContext.configService.setSchema(plugin.configPath, schema); + const configDescriptor = plugin.getConfigDescriptor(); + if (configDescriptor) { + this.pluginConfigDescriptors.set(plugin.name, configDescriptor); + await this.coreContext.configService.setSchema( + plugin.configPath, + configDescriptor.schema + ); } const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath); @@ -148,6 +207,10 @@ export class PluginsService implements CoreService { expect(thirdPluginToRun.setup).toHaveBeenCalledTimes(1); }); -test('`uiPlugins` returns empty Maps before plugins are added', async () => { - expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(` - Object { - "internal": Map {}, - "public": Map {}, - } - `); +test('`uiPlugins` returns empty Map before plugins are added', async () => { + expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(`Map {}`); }); test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { @@ -351,7 +346,7 @@ test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { pluginsSystem.addPlugin(plugin); }); - expect([...pluginsSystem.uiPlugins().internal.keys()]).toMatchInlineSnapshot(` + expect([...pluginsSystem.uiPlugins().keys()]).toMatchInlineSnapshot(` Array [ "order-0", "order-1", @@ -380,7 +375,7 @@ test('`uiPlugins` returns only ui plugin dependencies', async () => { pluginsSystem.addPlugin(plugin); }); - const plugin = pluginsSystem.uiPlugins().internal.get('ui-plugin')!; + const plugin = pluginsSystem.uiPlugins().get('ui-plugin')!; expect(plugin.requiredPlugins).toEqual(['req-ui']); expect(plugin.optionalPlugins).toEqual(['opt-ui']); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 34acb66d4e931..f437b51e5b07a 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -17,12 +17,10 @@ * under the License. */ -import { pick } from 'lodash'; - import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -158,33 +156,22 @@ export class PluginsSystem { const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter( pluginName => this.plugins.get(pluginName)!.includesUiPlugin ); - const internal = new Map( + const publicPlugins = new Map( uiPluginNames.map(pluginName => { const plugin = this.plugins.get(pluginName)!; return [ pluginName, { id: pluginName, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: plugin.manifest.requiredPlugins.filter(p => uiPluginNames.includes(p)), optionalPlugins: plugin.manifest.optionalPlugins.filter(p => uiPluginNames.includes(p)), }, - ] as [PluginName, DiscoveredPluginInternal]; + ]; }) ); - const publicPlugins = new Map( - [...internal.entries()].map( - ([pluginName, plugin]) => - [ - pluginName, - pick(plugin, ['id', 'configPath', 'requiredPlugins', 'optionalPlugins']), - ] as [PluginName, DiscoveredPlugin] - ) - ); - - return { public: publicPlugins, internal }; + return publicPlugins; } /** diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 9a3e922b3cb89..fd487d9fe00aa 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -24,7 +24,51 @@ import { ConfigPath, EnvironmentMode, PackageInfo } from '../config'; import { LoggerFactory } from '../logging'; import { CoreSetup, CoreStart } from '..'; -export type PluginConfigSchema = Type | null; +/** + * Dedicated type for plugin configuration schema. + * + * @public + */ +export type PluginConfigSchema = Type; + +/** + * Describes a plugin configuration schema and capabilities. + * + * @example + * ```typescript + * // my_plugin/server/index.ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * import { PluginConfigDescriptor } from 'kibana/server'; + * + * const configSchema = schema.object({ + * secret: schema.string({ defaultValue: 'Only on server' }), + * uiProp: schema.string({ defaultValue: 'Accessible from client' }), + * }); + * + * type ConfigType = TypeOf; + * + * export const config: PluginConfigDescriptor = { + * exposeToBrowser: { + * uiProp: true, + * }, + * schema: configSchema, + * }; + * ``` + * + * @public + */ +export interface PluginConfigDescriptor { + /** + * List of configuration properties that will be available on the client-side plugin. + */ + exposeToBrowser?: { [P in keyof T]?: boolean }; + /** + * Schema to use to validate the plugin configuration. + * + * {@link PluginConfigSchema} + */ + schema: PluginConfigSchema; +} /** * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays @@ -125,15 +169,14 @@ export interface DiscoveredPlugin { } /** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. * @internal */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { +export interface InternalPluginInfo { /** - * Path on the filesystem where plugin was loaded from. + * Path to the client-side entrypoint file to be used to build the client-side + * bundle for a plugin. */ - readonly path: string; + readonly entryPointPath: string; } /** diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 07bb4342c754a..c31ad90011865 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -54,7 +54,7 @@ describe('SavedObjectsService', () => { legacy: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; - await soService.setup(coreSetup); + await soService.setup(coreSetup, 1); return expect((KibanaMigrator as jest.Mock).mock.calls[0][0].callCluster()).resolves.toMatch( 'success' diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 5ccb02414d043..43c3afa3ed639 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -33,7 +33,7 @@ import { CoreContext } from '../core_context'; import { LegacyServiceSetup } from '../legacy/legacy_service'; import { ElasticsearchServiceSetup } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; -import { retryCallCluster } from '../elasticsearch/retry_call_cluster'; +import { retryCallCluster, migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { SavedObjectsConfigType } from './saved_objects_config'; import { KibanaRequest } from '../http'; import { Logger } from '..'; @@ -73,7 +73,10 @@ export class SavedObjectsService this.logger = coreContext.logger.get('savedobjects-service'); } - public async setup(coreSetup: SavedObjectsSetupDeps): Promise { + public async setup( + coreSetup: SavedObjectsSetupDeps, + migrationsRetryDelay?: number + ): Promise { this.logger.debug('Setting up SavedObjects service'); const { @@ -105,7 +108,11 @@ export class SavedObjectsService config: coreSetup.legacy.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, - callCluster: retryCallCluster(adminClient.callAsInternalUser), + callCluster: migrationsRetryCallCluster( + adminClient.callAsInternalUser, + this.coreContext.logger.get('migrations'), + migrationsRetryDelay + ), })); const mappings = this.migrator.getActiveMappings(); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 80a6a96aeaf2b..13a132ab9dd67 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; @@ -64,7 +64,7 @@ describe('Filter Utils', () => { test('Validate a simple filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) - ).toEqual(fromKueryExpression('foo.title: "best"')); + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { expect( @@ -74,7 +74,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -88,7 +88,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -102,7 +102,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' ) ); @@ -130,7 +130,7 @@ describe('Filter Utils', () => { describe('#validateFilterKueryNode', () => { test('Validate filter query through KueryNode - happy path', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -185,7 +185,7 @@ describe('Filter Utils', () => { test('Return Error if key is not wrapper by a saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -240,7 +240,7 @@ describe('Filter Utils', () => { test('Return Error if key of a saved object type is not wrapped with attributes', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), ['foo'], @@ -297,7 +297,7 @@ describe('Filter Utils', () => { test('Return Error if filter is not using an allowed type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -352,7 +352,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -405,5 +405,29 @@ describe('Filter Utils', () => { }, ]); }); + + test('Return Error if filter is using an non-existing key null key', () => { + const validationObject = validateFilterKueryNode( + esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), + ['foo'], + mockMappings + ); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: 'The key is empty and needs to be wrapped by a saved object type like foo', + isSavedObjectAttr: false, + key: null, + type: null, + }, + ]); + }); }); }); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 64abf268cacd6..3cf499de541ee 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -17,18 +17,18 @@ * under the License. */ -import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; import { get, set } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; +import { esKuery } from '../../../../../plugins/data/server'; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], filter: string, indexMapping: IndexMapping -): KueryNode => { +): esKuery.KueryNode | undefined => { if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = fromKueryExpression(filter); + const filterKueryNode = esKuery.fromKueryExpression(filter); const validationFilterKuery = validateFilterKueryNode( filterKueryNode, @@ -54,7 +54,7 @@ export const validateConvertFilterToKueryNode = ( validationFilterKuery.forEach(item => { const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); - const existingKueryNode: KueryNode = + const existingKueryNode: esKuery.KueryNode = path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; @@ -63,8 +63,8 @@ export const validateConvertFilterToKueryNode = ( set( filterKueryNode, path, - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', 'type', itemType[0]), + esKuery.nodeTypes.function.buildNode('and', [ + esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), existingKueryNode, ]) ); @@ -79,7 +79,6 @@ export const validateConvertFilterToKueryNode = ( }); return filterKueryNode; } - return null; }; interface ValidateFilterKueryNode { @@ -91,44 +90,48 @@ interface ValidateFilterKueryNode { } export const validateFilterKueryNode = ( - astFilter: KueryNode, + astFilter: esKuery.KueryNode, types: string[], indexMapping: IndexMapping, storeValue: boolean = false, path: string = 'arguments' ): ValidateFilterKueryNode[] => { - return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { - if (ast.arguments) { - const myPath = `${path}.${index}`; - return [ - ...kueryNode, - ...validateFilterKueryNode( - ast, - types, - indexMapping, - ast.type === 'function' && ['is', 'range'].includes(ast.function), - `${myPath}.arguments` - ), - ]; - } - if (storeValue && index === 0) { - const splitPath = path.split('.'); - return [ - ...kueryNode, - { - astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, indexMapping), - isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), - key: ast.value, - type: getType(ast.value), - }, - ]; - } - return kueryNode; - }, []); + return astFilter.arguments.reduce( + (kueryNode: string[], ast: esKuery.KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + indexMapping, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, indexMapping), + isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, + [] + ); }; -const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null); +const getType = (key: string | undefined | null) => + key != null && key.includes('.') ? key.split('.')[0] : null; /** * Is this filter key referring to a a top-level SavedObject attribute such as @@ -137,8 +140,8 @@ const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null); * @param key * @param indexMapping */ -export const isSavedObjectAttr = (key: string, indexMapping: IndexMapping) => { - const keySplit = key.split('.'); +export const isSavedObjectAttr = (key: string | null | undefined, indexMapping: IndexMapping) => { + const keySplit = key != null ? key.split('.') : []; if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) { return true; } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) { @@ -149,10 +152,13 @@ export const isSavedObjectAttr = (key: string, indexMapping: IndexMapping) => { }; export const hasFilterKeyError = ( - key: string, + key: string | null | undefined, types: string[], indexMapping: IndexMapping ): string | null => { + if (key == null) { + return `The key is empty and needs to be wrapped by a saved object type like ${types.join()}`; + } if (!key.includes('.')) { return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`; } else if (key.includes('.')) { diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 79a3e573ab98c..3d81c2c2efd52 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1289,8 +1289,7 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, - indexPattern: undefined, - kueryNode: null, + kueryNode: undefined, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 51d4a8ad50ad6..e8f1fb16461c1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -448,11 +448,11 @@ export class SavedObjectsRepository { } let kueryNode; + try { - kueryNode = - filter && filter !== '' - ? validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings) - : null; + if (filter) { + kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); + } } catch (e) { if (e.name === 'KQLSyntaxError') { throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index bee35b899d83c..cfeb258c2f03b 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; +import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; @@ -91,7 +91,7 @@ interface QueryParams { searchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } /** @@ -111,7 +111,7 @@ export function getQueryParams({ const types = getTypes(mappings, type); const bool: any = { filter: [ - ...(kueryNode != null ? [toElasticsearchQuery(kueryNode)] : []), + ...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 868ca51a76eab..f2bbc3ef564a1 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,13 +17,13 @@ * under the License. */ -import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { esKuery } from '../../../../../../plugins/data/server'; interface GetSearchDslOptions { type: string | string[]; @@ -37,7 +37,7 @@ interface GetSearchDslOptions { type: string; id: string; }; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } export function getSearchDsl( diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 97a04a4a4efab..d6cfa54397565 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -714,14 +714,15 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete:

(route: RouteConfig, handler: RequestHandler) => void; - get:

(route: RouteConfig, handler: RequestHandler) => void; + delete: RouteRegistrar; + get: RouteRegistrar; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; - post:

(route: RouteConfig, handler: RequestHandler) => void; - put:

(route: RouteConfig, handler: RequestHandler) => void; + handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; + post: RouteRegistrar; + put: RouteRegistrar; routerPath: string; } @@ -959,6 +960,17 @@ export interface Plugin { + exposeToBrowser?: { + [P in keyof T]?: boolean; + }; + schema: PluginConfigSchema; +} + +// @public +export type PluginConfigSchema = Type; + // @public export type PluginInitializer = (core: PluginInitializerContext) => Plugin; @@ -1004,8 +1016,9 @@ export interface PluginsServiceSetup { contracts: Map; // (undocumented) uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; } @@ -1087,6 +1100,9 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +// @public +export type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; + // @public (undocumented) export interface SavedObject { attributes: T; @@ -1615,6 +1631,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:38:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/ui_settings/create_objects_client_stub.ts b/src/core/server/ui_settings/create_objects_client_stub.ts deleted file mode 100644 index 1e4a5e6fb58ec..0000000000000 --- a/src/core/server/ui_settings/create_objects_client_stub.ts +++ /dev/null @@ -1,50 +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 sinon from 'sinon'; -import { SavedObjectsClient } from '../saved_objects'; - -export const savedObjectsClientErrors = SavedObjectsClient.errors; - -export interface SavedObjectsClientStub { - update: sinon.SinonStub; - get: sinon.SinonStub; - create: sinon.SinonStub; - bulkCreate: sinon.SinonStub; - bulkGet: sinon.SinonStub; - bulkUpdate: sinon.SinonStub; - delete: sinon.SinonStub; - find: sinon.SinonStub; - errors: typeof savedObjectsClientErrors; -} - -export function createObjectsClientStub(esDocSource = {}): SavedObjectsClientStub { - const savedObjectsClient = { - update: sinon.stub(), - get: sinon.stub().returns({ attributes: esDocSource }), - create: sinon.stub(), - errors: savedObjectsClientErrors, - bulkCreate: sinon.stub(), - bulkGet: sinon.stub(), - bulkUpdate: sinon.stub(), - delete: sinon.stub(), - find: sinon.stub(), - }; - return savedObjectsClient; -} diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts new file mode 100644 index 0000000000000..0b62aecc1d13f --- /dev/null +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const createOrUpgradeSavedConfigMock = jest.fn(); +jest.doMock('./create_or_upgrade_saved_config', () => ({ + createOrUpgradeSavedConfig: createOrUpgradeSavedConfigMock, +})); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 65b8792532acf..eab96cc80c883 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -17,19 +17,18 @@ * under the License. */ -import sinon from 'sinon'; import Chance from 'chance'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; - +import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; import { loggingServiceMock } from '../../logging/logging_service.mock'; -import * as getUpgradeableConfigNS from './get_upgradeable_config'; +import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; + import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; const chance = new Chance(); describe('uiSettings/createOrUpgradeSavedConfig', function() { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); + afterEach(() => jest.resetAllMocks()); const version = '4.0.1'; const prevVersion = '4.0.0'; @@ -37,14 +36,16 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { function setup() { const logger = loggingServiceMock.create(); - const getUpgradeableConfig = sandbox.stub(getUpgradeableConfigNS, 'getUpgradeableConfig'); - const savedObjectsClient = { - create: sinon.stub().callsFake(async (type, attributes, options = {}) => ({ - type, - id: options.id, - version: 'foo', - })), - } as any; // mute until we have savedObjects mocks + const getUpgradeableConfig = getUpgradeableConfigMock; + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.create.mockImplementation( + async (type, attributes, options = {}) => + ({ + type, + id: options.id, + version: 'foo', + } as any) + ); async function run(options = {}) { const resp = await createOrUpgradeSavedConfig({ @@ -56,8 +57,8 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { ...options, }); - sinon.assert.calledOnce(getUpgradeableConfig); - sinon.assert.alwaysCalledWith(getUpgradeableConfig, { savedObjectsClient, version }); + expect(getUpgradeableConfigMock).toHaveBeenCalledTimes(1); + expect(getUpgradeableConfig).toHaveBeenCalledWith({ savedObjectsClient, version }); return resp; } @@ -78,9 +79,8 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { await run(); - sinon.assert.calledOnce(savedObjectsClient.create); - sinon.assert.calledWithExactly( - savedObjectsClient.create, + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledWith( 'config', { buildNum, @@ -103,7 +103,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { [chance.word()]: chance.sentence(), }; - getUpgradeableConfig.resolves({ + getUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: savedAttributes, type: '', @@ -112,10 +112,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { await run(); - sinon.assert.calledOnce(getUpgradeableConfig); - sinon.assert.calledOnce(savedObjectsClient.create); - sinon.assert.calledWithExactly( - savedObjectsClient.create, + expect(getUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledWith( 'config', { ...savedAttributes, @@ -130,7 +129,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('should log a message for upgrades', async () => { const { getUpgradeableConfig, logger, run } = setup(); - getUpgradeableConfig.resolves({ + getUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, type: '', @@ -154,16 +153,14 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('does not log when upgrade fails', async () => { const { getUpgradeableConfig, logger, run, savedObjectsClient } = setup(); - getUpgradeableConfig.resolves({ + getUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, type: '', references: [], }); - savedObjectsClient.create.callsFake(async () => { - throw new Error('foo'); - }); + savedObjectsClient.create.mockRejectedValue(new Error('foo')); try { await run(); @@ -181,9 +178,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('throws write errors', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.callsFake(async () => { - throw error; - }); + savedObjectsClient.create.mockRejectedValue(error); await expect(run({ handleWriteErrors: false })).rejects.toThrowError(error); }); @@ -192,7 +187,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('returns undefined for ConflictError', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateConflictError(error)); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.decorateConflictError(error) + ); expect(await run({ handleWriteErrors: true })).toBe(undefined); }); @@ -200,7 +197,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('returns config attributes for NotAuthorizedError', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.throws( + savedObjectsClient.create.mockRejectedValue( SavedObjectsErrorHelpers.decorateNotAuthorizedError(error) ); @@ -212,7 +209,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('returns config attributes for ForbiddenError', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateForbiddenError(error)); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.decorateForbiddenError(error) + ); expect(await run({ handleWriteErrors: true })).toEqual({ buildNum, @@ -222,7 +221,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('throws error for other SavedObjects exceptions', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateGeneralError(error)); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.decorateGeneralError(error) + ); await expect(run({ handleWriteErrors: true })).rejects.toThrowError(error); }); @@ -230,7 +231,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('throws error for all other exceptions', async () => { const { run, savedObjectsClient } = setup(); const error = new Error('foo'); - savedObjectsClient.create.throws(error); + savedObjectsClient.create.mockRejectedValue(error); await expect(run({ handleWriteErrors: true })).rejects.toThrowError(error); }); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts new file mode 100644 index 0000000000000..f849b6bf5cdfa --- /dev/null +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const getUpgradeableConfigMock = jest.fn(); +jest.doMock('./get_upgradeable_config', () => ({ + getUpgradeableConfig: getUpgradeableConfigMock, +})); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 9d52a339ccf91..f7dbf992e8728 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import expect from '@kbn/expect'; import { SavedObjectsClientContract } from 'src/core/server'; import { @@ -97,16 +96,14 @@ describe('createOrUpgradeSavedConfig()', () => { }); const config540 = await savedObjectsClient.get('config', '5.4.0'); - expect(config540) - .to.have.property('attributes') - .eql({ - // should have the new build number - buildNum: 54099, + expect(config540.attributes).toEqual({ + // should have the new build number + buildNum: 54099, - // 5.4.0-SNAPSHOT and @@version were ignored so we only have the - // attributes from 5.4.0-rc1, even though the other build nums are greater - '5.4.0-rc1': true, - }); + // 5.4.0-SNAPSHOT and @@version were ignored so we only have the + // attributes from 5.4.0-rc1, even though the other build nums are greater + '5.4.0-rc1': true, + }); // add the 5.4.0 flag to the 5.4.0 savedConfig await savedObjectsClient.update('config', '5.4.0', { @@ -124,16 +121,14 @@ describe('createOrUpgradeSavedConfig()', () => { }); const config541 = await savedObjectsClient.get('config', '5.4.1'); - expect(config541) - .to.have.property('attributes') - .eql({ - // should have the new build number - buildNum: 54199, + expect(config541.attributes).toEqual({ + // should have the new build number + buildNum: 54199, - // should also include properties from 5.4.0 and 5.4.0-rc1 - '5.4.0': true, - '5.4.0-rc1': true, - }); + // should also include properties from 5.4.0 and 5.4.0-rc1 + '5.4.0': true, + '5.4.0-rc1': true, + }); // add the 5.4.1 flag to the 5.4.1 savedConfig await savedObjectsClient.update('config', '5.4.1', { @@ -151,17 +146,15 @@ describe('createOrUpgradeSavedConfig()', () => { }); const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); - expect(config700rc1) - .to.have.property('attributes') - .eql({ - // should have the new build number - buildNum: 70010, - - // should also include properties from 5.4.1, 5.4.0 and 5.4.0-rc1 - '5.4.1': true, - '5.4.0': true, - '5.4.0-rc1': true, - }); + expect(config700rc1.attributes).toEqual({ + // should have the new build number + buildNum: 70010, + + // should also include properties from 5.4.1, 5.4.0 and 5.4.0-rc1 + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); // tag the 7.0.0-rc1 doc await savedObjectsClient.update('config', '7.0.0-rc1', { @@ -179,18 +172,16 @@ describe('createOrUpgradeSavedConfig()', () => { }); const config700 = await savedObjectsClient.get('config', '7.0.0'); - expect(config700) - .to.have.property('attributes') - .eql({ - // should have the new build number - buildNum: 70099, - - // should also include properties from ancestors, including 7.0.0-rc1 - '7.0.0-rc1': true, - '5.4.1': true, - '5.4.0': true, - '5.4.0-rc1': true, - }); + expect(config700.attributes).toEqual({ + // should have the new build number + buildNum: 70099, + + // should also include properties from ancestors, including 7.0.0-rc1 + '7.0.0-rc1': true, + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); // tag the 7.0.0 doc await savedObjectsClient.update('config', '7.0.0', { @@ -208,16 +199,14 @@ describe('createOrUpgradeSavedConfig()', () => { }); const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); - expect(config623rc1) - .to.have.property('attributes') - .eql({ - // should have the new build number - buildNum: 62310, - - // should also include properties from ancestors, but not 7.0.0-rc1 or 7.0.0 - '5.4.1': true, - '5.4.0': true, - '5.4.0-rc1': true, - }); + expect(config623rc1.attributes).toEqual({ + // should have the new build number + buildNum: 62310, + + // should also include properties from ancestors, but not 7.0.0-rc1 or 7.0.0 + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); }); }); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts index 073a6961fdec4..feb63817fe073 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts @@ -17,14 +17,12 @@ * under the License. */ -import expect from '@kbn/expect'; - import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; describe('savedObjects/health_check/isConfigVersionUpgradeable', function() { function isUpgradeableTest(savedVersion: string, kibanaVersion: string, expected: boolean) { it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => { - expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected); + expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).toBe(expected); }); } diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index 7783fd9976963..031464c7fdaff 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -17,10 +17,7 @@ * under the License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { getServices, chance, assertSinonMatch } from './lib'; +import { getServices, chance } from './lib'; export function docExistsSuite() { async function setup(options: any = {}) { @@ -58,11 +55,11 @@ export function docExistsSuite() { url: '/api/kibana/settings', }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, defaultIndex: { userValue: defaultIndex, @@ -89,11 +86,12 @@ export function docExistsSuite() { }, }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, defaultIndex: { userValue: defaultIndex, @@ -117,8 +115,8 @@ export function docExistsSuite() { }, }); - expect(statusCode).to.be(400); - assertSinonMatch(result, { + expect(statusCode).toBe(400); + expect(result).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -141,11 +139,12 @@ export function docExistsSuite() { }, }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, defaultIndex: { userValue: defaultIndex, @@ -171,8 +170,8 @@ export function docExistsSuite() { }, }); - expect(statusCode).to.be(400); - assertSinonMatch(result, { + expect(statusCode).toBe(400); + expect(result).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -188,18 +187,18 @@ export function docExistsSuite() { initialSettings: { defaultIndex }, }); - expect(await uiSettings.get('defaultIndex')).to.be(defaultIndex); + expect(await uiSettings.get('defaultIndex')).toBe(defaultIndex); const { statusCode, result } = await kbnServer.inject({ method: 'DELETE', url: '/api/kibana/settings/defaultIndex', }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, foo: { userValue: 'bar', @@ -216,8 +215,8 @@ export function docExistsSuite() { url: '/api/kibana/settings/foo', }); - expect(statusCode).to.be(400); - assertSinonMatch(result, { + expect(statusCode).toBe(400); + expect(result).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index 580fe04b92087..f535f237c11de 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -17,10 +17,7 @@ * under the License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { getServices, chance, assertSinonMatch } from './lib'; +import { getServices, chance } from './lib'; export function docMissingSuite() { // ensure the kibana index has no documents @@ -52,11 +49,11 @@ export function docMissingSuite() { url: '/api/kibana/settings', }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, foo: { userValue: 'bar', @@ -78,11 +75,11 @@ export function docMissingSuite() { payload: { value: defaultIndex }, }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, defaultIndex: { userValue: defaultIndex, @@ -109,11 +106,11 @@ export function docMissingSuite() { }, }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, defaultIndex: { userValue: defaultIndex, @@ -136,11 +133,11 @@ export function docMissingSuite() { url: '/api/kibana/settings/defaultIndex', }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, foo: { userValue: 'bar', diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts index 1a17970081d9c..5ac83aee52a65 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts @@ -17,10 +17,7 @@ * under the License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { getServices, chance, assertSinonMatch } from './lib'; +import { getServices, chance } from './lib'; export function docMissingAndIndexReadOnlySuite() { // ensure the kibana index has no documents @@ -80,11 +77,12 @@ export function docMissingAndIndexReadOnlySuite() { url: '/api/kibana/settings', }); - expect(statusCode).to.be(200); - assertSinonMatch(result, { + expect(statusCode).toBe(200); + + expect(result).toMatchObject({ settings: { buildNum: { - userValue: sinon.match.number, + userValue: expect.any(Number), }, foo: { userValue: 'bar', @@ -106,10 +104,11 @@ export function docMissingAndIndexReadOnlySuite() { payload: { value: defaultIndex }, }); - expect(statusCode).to.be(403); - assertSinonMatch(result, { + expect(statusCode).toBe(403); + + expect(result).toEqual({ error: 'Forbidden', - message: sinon.match('index read-only'), + message: expect.stringContaining('index read-only'), statusCode: 403, }); }); @@ -128,10 +127,10 @@ export function docMissingAndIndexReadOnlySuite() { }, }); - expect(statusCode).to.be(403); - assertSinonMatch(result, { + expect(statusCode).toBe(403); + expect(result).toEqual({ error: 'Forbidden', - message: sinon.match('index read-only'), + message: expect.stringContaining('index read-only'), statusCode: 403, }); }); @@ -146,10 +145,10 @@ export function docMissingAndIndexReadOnlySuite() { url: '/api/kibana/settings/defaultIndex', }); - expect(statusCode).to.be(403); - assertSinonMatch(result, { + expect(statusCode).toBe(403); + expect(result).toEqual({ error: 'Forbidden', - message: sinon.match('index read-only'), + message: expect.stringContaining('index read-only'), statusCode: 403, }); }); diff --git a/src/core/server/ui_settings/integration_tests/lib/assert.ts b/src/core/server/ui_settings/integration_tests/lib/assert.ts deleted file mode 100644 index 62533b7ae734d..0000000000000 --- a/src/core/server/ui_settings/integration_tests/lib/assert.ts +++ /dev/null @@ -1,26 +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 sinon from 'sinon'; - -export function assertSinonMatch(value: any, match: any) { - const stub = sinon.stub(); - stub(value); - sinon.assert.calledWithExactly(stub, match); -} diff --git a/src/core/server/ui_settings/integration_tests/lib/index.ts b/src/core/server/ui_settings/integration_tests/lib/index.ts index 33a1cbd4d780b..b8349e5e41ccb 100644 --- a/src/core/server/ui_settings/integration_tests/lib/index.ts +++ b/src/core/server/ui_settings/integration_tests/lib/index.ts @@ -20,5 +20,3 @@ export { startServers, getServices, stopServers } from './servers'; export { chance } from './chance'; - -export { assertSinonMatch } from './assert'; diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 1c99637a89fed..b8aa57291dccf 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -17,16 +17,15 @@ * under the License. */ -import expect from '@kbn/expect'; import Chance from 'chance'; -import sinon from 'sinon'; import { loggingServiceMock } from '../logging/logging_service.mock'; +import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock'; +import { SavedObjectsClient } from '../saved_objects'; +import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; import { UiSettingsClient } from './ui_settings_client'; import { CannotOverrideError } from './ui_settings_errors'; -import * as createOrUpgradeSavedConfigNS from './create_or_upgrade_saved_config/create_or_upgrade_saved_config'; -import { createObjectsClientStub, savedObjectsClientErrors } from './create_objects_client_stub'; const logger = loggingServiceMock.create().get(); @@ -42,12 +41,11 @@ interface SetupOptions { } describe('ui settings', () => { - const sandbox = sinon.createSandbox(); - function setup(options: SetupOptions = {}) { const { defaults = {}, overrides = {}, esDocSource = {} } = options; - const savedObjectsClient = createObjectsClientStub(esDocSource); + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue({ attributes: esDocSource } as any); const uiSettings = new UiSettingsClient({ type: TYPE, @@ -59,92 +57,74 @@ describe('ui settings', () => { log: logger, }); - const createOrUpgradeSavedConfig = sandbox.stub( - createOrUpgradeSavedConfigNS, - 'createOrUpgradeSavedConfig' - ); - - function assertGetQuery() { - sinon.assert.calledOnce(savedObjectsClient.get); - - const { args } = savedObjectsClient.get.getCall(0); - expect(args[0]).to.be(TYPE); - expect(args[1]).to.eql(ID); - } - - function assertUpdateQuery(expectedChanges: unknown) { - sinon.assert.calledOnce(savedObjectsClient.update); - - const { args } = savedObjectsClient.update.getCall(0); - expect(args[0]).to.be(TYPE); - expect(args[1]).to.eql(ID); - expect(args[2]).to.eql(expectedChanges); - } + const createOrUpgradeSavedConfig = createOrUpgradeSavedConfigMock; return { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig, - assertGetQuery, - assertUpdateQuery, }; } - afterEach(() => sandbox.restore()); + afterEach(() => jest.clearAllMocks()); describe('#setMany()', () => { it('returns a promise', () => { const { uiSettings } = setup(); - expect(uiSettings.setMany({ a: 'b' })).to.be.a(Promise); + expect(uiSettings.setMany({ a: 'b' })).toBeInstanceOf(Promise); }); it('updates a single value in one operation', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.setMany({ one: 'value' }); - assertUpdateQuery({ one: 'value' }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { one: 'value' }); }); it('updates several values in one operation', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.setMany({ one: 'value', another: 'val' }); - assertUpdateQuery({ one: 'value', another: 'val' }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { + one: 'value', + another: 'val', + }); }); it('automatically creates the savedConfig if it is missing', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); savedObjectsClient.update - .onFirstCall() - .throws(savedObjectsClientErrors.createGenericNotFoundError()) - .onSecondCall() - .returns({}); + .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) + .mockResolvedValueOnce({} as any); await uiSettings.setMany({ foo: 'bar' }); - sinon.assert.calledTwice(savedObjectsClient.update); - sinon.assert.calledOnce(createOrUpgradeSavedConfig); - sinon.assert.calledWith( - createOrUpgradeSavedConfig, - sinon.match({ handleWriteErrors: false }) + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect.objectContaining({ handleWriteErrors: false }) ); }); it('only tried to auto create once and throws NotFound', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - savedObjectsClient.update.throws(savedObjectsClientErrors.createGenericNotFoundError()); + savedObjectsClient.update.mockRejectedValue( + SavedObjectsClient.errors.createGenericNotFoundError() + ); try { await uiSettings.setMany({ foo: 'bar' }); throw new Error('expected setMany to throw a NotFound error'); } catch (error) { - expect(savedObjectsClientErrors.isNotFoundError(error)).to.be(true); + expect(SavedObjectsClient.errors.isNotFoundError(error)).toBe(true); } - sinon.assert.calledTwice(savedObjectsClient.update); - sinon.assert.calledOnce(createOrUpgradeSavedConfig); - - sinon.assert.calledWith( - createOrUpgradeSavedConfig, - sinon.match({ handleWriteErrors: false }) + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect.objectContaining({ handleWriteErrors: false }) ); }); @@ -161,8 +141,8 @@ describe('ui settings', () => { foo: 'baz', }); } catch (error) { - expect(error).to.be.a(CannotOverrideError); - expect(error.message).to.be('Unable to update "foo" because it is overridden'); + expect(error).toBeInstanceOf(CannotOverrideError); + expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); }); @@ -170,13 +150,17 @@ describe('ui settings', () => { describe('#set()', () => { it('returns a promise', () => { const { uiSettings } = setup(); - expect(uiSettings.set('a', 'b')).to.be.a(Promise); + expect(uiSettings.set('a', 'b')).toBeInstanceOf(Promise); }); it('updates single values by (key, value)', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.set('one', 'value'); - assertUpdateQuery({ one: 'value' }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { + one: 'value', + }); }); it('throws CannotOverrideError if the key is overridden', async () => { @@ -189,8 +173,8 @@ describe('ui settings', () => { try { await uiSettings.set('foo', 'baz'); } catch (error) { - expect(error).to.be.a(CannotOverrideError); - expect(error.message).to.be('Unable to update "foo" because it is overridden'); + expect(error).toBeInstanceOf(CannotOverrideError); + expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); }); @@ -198,13 +182,15 @@ describe('ui settings', () => { describe('#remove()', () => { it('returns a promise', () => { const { uiSettings } = setup(); - expect(uiSettings.remove('one')).to.be.a(Promise); + expect(uiSettings.remove('one')).toBeInstanceOf(Promise); }); it('removes single values by key', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.remove('one'); - assertUpdateQuery({ one: null }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { one: null }); }); it('throws CannotOverrideError if the key is overridden', async () => { @@ -217,8 +203,8 @@ describe('ui settings', () => { try { await uiSettings.remove('foo'); } catch (error) { - expect(error).to.be.a(CannotOverrideError); - expect(error.message).to.be('Unable to update "foo" because it is overridden'); + expect(error).toBeInstanceOf(CannotOverrideError); + expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); }); @@ -226,19 +212,27 @@ describe('ui settings', () => { describe('#removeMany()', () => { it('returns a promise', () => { const { uiSettings } = setup(); - expect(uiSettings.removeMany(['one'])).to.be.a(Promise); + expect(uiSettings.removeMany(['one'])).toBeInstanceOf(Promise); }); it('removes a single value', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.removeMany(['one']); - assertUpdateQuery({ one: null }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { one: null }); }); it('updates several values in one operation', async () => { - const { uiSettings, assertUpdateQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.removeMany(['one', 'two', 'three']); - assertUpdateQuery({ one: null, two: null, three: null }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { + one: null, + two: null, + three: null, + }); }); it('throws CannotOverrideError if any key is overridden', async () => { @@ -251,8 +245,8 @@ describe('ui settings', () => { try { await uiSettings.setMany({ baz: 'baz', foo: 'foo' }); } catch (error) { - expect(error).to.be.a(CannotOverrideError); - expect(error.message).to.be('Unable to update "foo" because it is overridden'); + expect(error).toBeInstanceOf(CannotOverrideError); + expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); }); @@ -262,22 +256,25 @@ describe('ui settings', () => { const value = chance.word(); const defaults = { key: { value } }; const { uiSettings } = setup({ defaults }); - expect(uiSettings.getRegistered()).to.be(defaults); + expect(uiSettings.getRegistered()).toBe(defaults); }); }); describe('#getUserProvided()', () => { it('pulls user configuration from ES', async () => { - const { uiSettings, assertGetQuery } = setup(); + const { uiSettings, savedObjectsClient } = setup(); await uiSettings.getUserProvided(); - assertGetQuery(); + + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); }); it('returns user configuration', async () => { const esDocSource = { user: 'customized' }; const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).to.eql({ + + expect(result).toEqual({ user: { userValue: 'customized', }, @@ -288,7 +285,8 @@ describe('ui settings', () => { const esDocSource = { user: 'customized', usingDefault: null, something: 'else' }; const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).to.eql({ + + expect(result).toEqual({ user: { userValue: 'customized', }, @@ -300,59 +298,62 @@ describe('ui settings', () => { it('automatically creates the savedConfig if it is missing and returns empty object', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - savedObjectsClient.get - .onFirstCall() - .throws(savedObjectsClientErrors.createGenericNotFoundError()) - .onSecondCall() - .returns({ attributes: {} }); + savedObjectsClient.get = jest + .fn() + .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) + .mockResolvedValueOnce({ attributes: {} }); - expect(await uiSettings.getUserProvided()).to.eql({}); + expect(await uiSettings.getUserProvided()).toEqual({}); - sinon.assert.calledTwice(savedObjectsClient.get); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); - sinon.assert.calledOnce(createOrUpgradeSavedConfig); - sinon.assert.calledWith(createOrUpgradeSavedConfig, sinon.match({ handleWriteErrors: true })); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect.objectContaining({ handleWriteErrors: true }) + ); }); it('returns result of savedConfig creation in case of notFound error', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - createOrUpgradeSavedConfig.resolves({ foo: 'bar ' }); - savedObjectsClient.get.throws(savedObjectsClientErrors.createGenericNotFoundError()); + createOrUpgradeSavedConfig.mockResolvedValue({ foo: 'bar ' }); + savedObjectsClient.get.mockRejectedValue( + SavedObjectsClient.errors.createGenericNotFoundError() + ); - expect(await uiSettings.getUserProvided()).to.eql({ foo: { userValue: 'bar ' } }); + expect(await uiSettings.getUserProvided()).toEqual({ foo: { userValue: 'bar ' } }); }); it('returns an empty object on Forbidden responses', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - const error = savedObjectsClientErrors.decorateForbiddenError(new Error()); - savedObjectsClient.get.throws(error); + const error = SavedObjectsClient.errors.decorateForbiddenError(new Error()); + savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).to.eql({}); - sinon.assert.notCalled(createOrUpgradeSavedConfig); + expect(await uiSettings.getUserProvided()).toEqual({}); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('returns an empty object on EsUnavailable responses', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - const error = savedObjectsClientErrors.decorateEsUnavailableError(new Error()); - savedObjectsClient.get.throws(error); + const error = SavedObjectsClient.errors.decorateEsUnavailableError(new Error()); + savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).to.eql({}); - sinon.assert.notCalled(createOrUpgradeSavedConfig); + expect(await uiSettings.getUserProvided()).toEqual({}); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('throws Unauthorized errors', async () => { const { uiSettings, savedObjectsClient } = setup(); - const error = savedObjectsClientErrors.decorateNotAuthorizedError(new Error()); - savedObjectsClient.get.throws(error); + const error = SavedObjectsClient.errors.decorateNotAuthorizedError(new Error()); + savedObjectsClient.get.mockRejectedValue(error); try { await uiSettings.getUserProvided(); throw new Error('expect getUserProvided() to throw'); } catch (err) { - expect(err).to.be(error); + expect(err).toBe(error); } }); @@ -360,13 +361,13 @@ describe('ui settings', () => { const { uiSettings, savedObjectsClient } = setup(); const error = new Error('unexpected'); - savedObjectsClient.get.throws(error); + savedObjectsClient.get.mockRejectedValue(error); try { await uiSettings.getUserProvided(); throw new Error('expect getUserProvided() to throw'); } catch (err) { - expect(err).to.be(error); + expect(err).toBe(error); } }); @@ -381,7 +382,7 @@ describe('ui settings', () => { }; const { uiSettings } = setup({ esDocSource, overrides }); - expect(await uiSettings.getUserProvided()).to.eql({ + expect(await uiSettings.getUserProvided()).toEqual({ user: { userValue: 'customized', }, @@ -397,16 +398,17 @@ describe('ui settings', () => { describe('#getAll()', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; - const { uiSettings, assertGetQuery } = setup({ esDocSource }); + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); await uiSettings.getAll(); - assertGetQuery(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); }); it(`returns defaults when es doc is empty`, async () => { const esDocSource = {}; const defaults = { foo: { value: 'bar' } }; const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).to.eql({ + expect(await uiSettings.getAll()).toEqual({ foo: 'bar', }); }); @@ -424,7 +426,8 @@ describe('ui settings', () => { }; const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).to.eql({ + + expect(await uiSettings.getAll()).toEqual({ foo: 'user-override', bar: 'user-provided', }); @@ -447,7 +450,8 @@ describe('ui settings', () => { }; const { uiSettings } = setup({ esDocSource, defaults, overrides }); - expect(await uiSettings.getAll()).to.eql({ + + expect(await uiSettings.getAll()).toEqual({ foo: 'bax', bar: 'user-provided', }); @@ -457,9 +461,11 @@ describe('ui settings', () => { describe('#get()', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; - const { uiSettings, assertGetQuery } = setup({ esDocSource }); + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); await uiSettings.get('any'); - assertGetQuery(); + + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); }); it(`returns the promised value for a key`, async () => { @@ -467,28 +473,31 @@ describe('ui settings', () => { const defaults = { dateFormat: { value: chance.word() } }; const { uiSettings } = setup({ esDocSource, defaults }); const result = await uiSettings.get('dateFormat'); - expect(result).to.equal(defaults.dateFormat.value); + + expect(result).toBe(defaults.dateFormat.value); }); it(`returns the user-configured value for a custom key`, async () => { const esDocSource = { custom: 'value' }; const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.get('custom'); - expect(result).to.equal('value'); + + expect(result).toBe('value'); }); it(`returns the user-configured value for a modified key`, async () => { const esDocSource = { dateFormat: 'YYYY-MM-DD' }; const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.get('dateFormat'); - expect(result).to.equal('YYYY-MM-DD'); + expect(result).toBe('YYYY-MM-DD'); }); it('returns the overridden value for an overrided key', async () => { const esDocSource = { dateFormat: 'YYYY-MM-DD' }; const overrides = { dateFormat: 'foo' }; const { uiSettings } = setup({ esDocSource, overrides }); - expect(await uiSettings.get('dateFormat')).to.be('foo'); + + expect(await uiSettings.get('dateFormat')).toBe('foo'); }); it('returns the default value for an override with value null', async () => { @@ -496,35 +505,40 @@ describe('ui settings', () => { const overrides = { dateFormat: null }; const defaults = { dateFormat: { value: 'foo' } }; const { uiSettings } = setup({ esDocSource, overrides, defaults }); - expect(await uiSettings.get('dateFormat')).to.be('foo'); + + expect(await uiSettings.get('dateFormat')).toBe('foo'); }); it('returns the overridden value if the document does not exist', async () => { const overrides = { dateFormat: 'foo' }; const { uiSettings, savedObjectsClient } = setup({ overrides }); - savedObjectsClient.get - .onFirstCall() - .throws(savedObjectsClientErrors.createGenericNotFoundError()); - expect(await uiSettings.get('dateFormat')).to.be('foo'); + savedObjectsClient.get.mockRejectedValueOnce( + SavedObjectsClient.errors.createGenericNotFoundError() + ); + + expect(await uiSettings.get('dateFormat')).toBe('foo'); }); }); describe('#isOverridden()', () => { it('returns false if no overrides defined', () => { const { uiSettings } = setup(); - expect(uiSettings.isOverridden('foo')).to.be(false); + expect(uiSettings.isOverridden('foo')).toBe(false); }); + it('returns false if overrides defined but key is not included', () => { const { uiSettings } = setup({ overrides: { foo: true, bar: true } }); - expect(uiSettings.isOverridden('baz')).to.be(false); + expect(uiSettings.isOverridden('baz')).toBe(false); }); + it('returns false for object prototype properties', () => { const { uiSettings } = setup({ overrides: { foo: true, bar: true } }); - expect(uiSettings.isOverridden('hasOwnProperty')).to.be(false); + expect(uiSettings.isOverridden('hasOwnProperty')).toBe(false); }); + it('returns true if overrides defined and key is overridden', () => { const { uiSettings } = setup({ overrides: { foo: true, bar: true } }); - expect(uiSettings.isOverridden('bar')).to.be(true); + expect(uiSettings.isOverridden('bar')).toBe(true); }); }); }); diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index eeffc2380f38b..d78a0654646c3 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -30,7 +30,6 @@ import { CleanTypescriptTask, CleanNodeBuildsTask, CleanTask, - CleanCtagBuildTask, CopySourceTask, CreateArchivesSourcesTask, CreateArchivesTask, @@ -133,7 +132,6 @@ export async function buildDistributables(options) { await run(CleanExtraBinScriptsTask); await run(CleanExtraBrowsersTask); await run(CleanNodeBuildsTask); - await run(CleanCtagBuildTask); await run(PathLengthTask); diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js index 3ec1d6b6967e9..b23db67cc1b07 100644 --- a/src/dev/build/tasks/clean_tasks.js +++ b/src/dev/build/tasks/clean_tasks.js @@ -20,9 +20,6 @@ import minimatch from 'minimatch'; import { deleteAll, deleteEmptyFolders, scanDelete } from '../lib'; -import { resolve } from 'path'; - -const RELATIVE_CTAGS_BUILD_DIR = 'node_modules/@elastic/node-ctags/ctags/build'; export const CleanTask = { global: true, @@ -169,7 +166,6 @@ export const CleanExtraFilesFromModulesTask = { await scanDelete({ directory: build.resolvePath('node_modules'), regularExpressions, - excludePaths: [build.resolvePath('node_modules/@elastic/ctags-langserver/vendor')], }) ); @@ -257,39 +253,3 @@ export const CleanEmptyFoldersTask = { ]); }, }; - -export const CleanCtagBuildTask = { - description: 'Cleaning extra platform-specific files from @elastic/node-ctag build dir', - - async run(config, log, build) { - const getPlatformId = platform => { - if (platform.isWindows()) { - return 'win32'; - } else if (platform.isLinux()) { - return 'linux'; - } else if (platform.isMac()) { - return 'darwin'; - } - }; - - await Promise.all( - config.getTargetPlatforms().map(async platform => { - if (build.isOss()) { - return; - } - - const ctagsBuildDir = build.resolvePathForPlatform(platform, RELATIVE_CTAGS_BUILD_DIR); - await deleteAll( - [ - resolve(ctagsBuildDir, '*'), - `!${resolve( - ctagsBuildDir, - `ctags-node-v${process.versions.modules}-${getPlatformId(platform)}-x64` - )}`, - ], - log - ); - }) - ); - }, -}; diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 5c0462ce86fa9..b0e38b6481457 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -66,10 +66,38 @@ export const CleanClientModulesOnDLLTask = { // side code entries that were provided const serverDependencies = await getDependencies(baseDir, serverEntries); + // This fulfill a particular exceptional case where + // we need to keep loading a file from a node_module + // only used in the front-end like we do when using the file-loader + // in https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js + // + // manual list of exception modules + const manualExceptionModules = [ + 'mapbox-gl' + ]; + + // consider the top modules as exceptions as the entry points + // to look for other exceptions dependent on that one + const manualExceptionEntries = [ + ...manualExceptionModules.map(module => `${baseDir}/node_modules/${module}`) + ]; + + // dependencies for declared exception modules + const manualExceptionModulesDependencies = await getDependencies(baseDir, [ + ...manualExceptionEntries + ]); + + // final list of manual exceptions to add + const manualExceptions = [ + ...manualExceptionModules, + ...manualExceptionModulesDependencies + ]; + // Consider this as our whiteList for the modules we can't delete const whiteListedModules = [ ...serverDependencies, - ...kbnWebpackLoaders + ...kbnWebpackLoaders, + ...manualExceptions ]; // Resolve the client vendors dll manifest path diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 6609b905b81ec..0c8faf47411d4 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,9 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.session.idleTimeout + xpack.security.session.lifespan + xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom ) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 805b77365e624..6cfcaca5843b3 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -22,6 +22,15 @@ C_RESET='\033[0m' # Reset color ### export FORCE_COLOR=1 +### +### The @babel/register cache collects the build output from each file in +### a map, in memory, and then when the process exits it writes that to the +### babel cache file as a JSON encoded object. Stringifying that object +### causes OOMs on CI regularly enough that we need to find another solution, +### and until we do we need to disable the cache +### +export BABEL_DISABLE_CACHE=true + ### ### check that we seem to be in a kibana project ### diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 30501965bf1e7..7f51326ee46bb 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -45,7 +45,7 @@ export default class JestJUnitReporter { * @return {undefined} */ onRunComplete(contexts, results) { - if (!process.env.CI || !results.testResults.length) { + if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index fbd16d95ded1c..a4aa3474c0762 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -105,6 +105,4 @@ export const LICENSE_OVERRIDES = { // TODO can be removed once we upgrade the use of walk dependency past or equal to v2.3.14 'walk@2.3.9': ['MIT'], - - '@elastic/node-ctags@1.0.2': ['Nuclide software'], }; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index edd818e1b42de..e18852e353b00 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -167,7 +167,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', - 'x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email-influencers.html', 'x-pack/legacy/plugins/monitoring/public/icons/alert-blue.svg', 'x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg', 'x-pack/legacy/plugins/monitoring/public/icons/health-green.svg', diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index 7e62059f5059a..6acbbaa4d5255 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -21,7 +21,7 @@ import { RENOVATE_PACKAGE_GROUPS } from './package_groups'; import { PACKAGE_GLOBS } from './package_globs'; import { wordRegExp, maybeFlatMap, maybeMap, getTypePackageName } from './utils'; -const DEFAULT_LABELS = ['release_note:skip', 'renovate', 'v8.0.0', 'v7.6.0']; +const DEFAULT_LABELS = ['release_note:skip', 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0']; export const RENOVATE_CONFIG = { extends: ['config:base'], diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index e1fa5db8b7140..9f6d648477d29 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -21,6 +21,7 @@ import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; +import { mockUiSettings } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; export default function stubbedLogstashIndexPatternService() { const mockLogstashFields = stubbedLogstashFields(); @@ -40,7 +41,7 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern('logstash-*', cfg => cfg, 'time', fields); + const indexPattern = new StubIndexPattern('logstash-*', cfg => cfg, 'time', fields, mockUiSettings); indexPattern.id = 'logstash-*'; indexPattern.isTimeNanosBased = () => false; diff --git a/src/fixtures/stubbed_search_source.js b/src/fixtures/stubbed_search_source.js index 3a36b97e6757e..da741a1aa4774 100644 --- a/src/fixtures/stubbed_search_source.js +++ b/src/fixtures/stubbed_search_source.js @@ -60,9 +60,6 @@ export default function stubSearchSource(Private, $q, Promise) { onRequestStart(fn) { this._requestStartHandlers.push(fn); }, - requestIsStarting(req) { - return Promise.map(this._requestStartHandlers, fn => fn(req)); - }, requestIsStopped() {} }; diff --git a/src/legacy/core_plugins/apm_oss/index.js b/src/legacy/core_plugins/apm_oss/index.js index 0c281ec939bb1..9de571ab7cae9 100644 --- a/src/legacy/core_plugins/apm_oss/index.js +++ b/src/legacy/core_plugins/apm_oss/index.js @@ -38,7 +38,6 @@ export default function apmOss(kibana) { spanIndices: Joi.string().default('apm-*'), metricsIndices: Joi.string().default('apm-*'), onboardingIndices: Joi.string().default('apm-*'), - apmAgentConfigurationIndex: Joi.string().default('.apm-agent-configuration'), }).default(); }, diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx index 33cefd9b20968..01bd3fcd78e53 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx @@ -41,7 +41,7 @@ export function EditorExample(props: EditorExampleProps) { return () => { editor.destroy(); }; - }, []); + }, [elemId]); return

; } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx index 747c21433f8ed..80960a7772ba1 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx @@ -41,7 +41,7 @@ export function Panel({ children, initialWidth = '100%', style = {} }: Props) { return divRef.current!.getBoundingClientRect().width; }, }); - }, []); + }, [initialWidth, registry]); return (
diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx index fdfe9ecc7b94c..30966a2f77e1d 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import moment from 'moment'; @@ -32,9 +32,10 @@ import { EuiButton, } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext } from '../../contexts'; import { HistoryViewer } from './history_viewer'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { useEditorReadContext } from '../../contexts/editor_context'; +import { useRestoreRequestFromHistory } from '../../hooks'; interface Props { close: () => void; @@ -45,9 +46,8 @@ const CHILD_ELEMENT_PREFIX = 'historyReq'; export function ConsoleHistory({ close }: Props) { const { services: { history }, - } = useAppContext(); + } = useServicesContext(); - const dispatch = useEditorActionContext(); const { settings: readOnlySettings } = useEditorReadContext(); const [requests, setPastRequests] = useState(history.getHistory()); @@ -55,7 +55,7 @@ export function ConsoleHistory({ close }: Props) { const clearHistory = useCallback(() => { history.clearHistory(); setPastRequests(history.getHistory()); - }, []); + }, [history]); const listRef = useRef(null); @@ -63,14 +63,7 @@ export function ConsoleHistory({ close }: Props) { const [selectedIndex, setSelectedIndex] = useState(0); const selectedReq = useRef(null); - const scrollIntoView = (idx: number) => { - const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); - if (activeDescendant) { - activeDescendant.scrollIntoView(); - } - }; - - const [describeReq] = useState(() => { + const describeReq = useMemo(() => { const _describeReq = (req: any) => { const endpoint = req.endpoint; const date = moment(req.time); @@ -86,34 +79,39 @@ export function ConsoleHistory({ close }: Props) { (_describeReq as any).cache = new WeakMap(); return memoize(_describeReq); - }); + }, []); + + const scrollIntoView = useCallback((idx: number) => { + const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); + if (activeDescendant) { + activeDescendant.scrollIntoView(); + } + }, []); - const initialize = () => { + const initialize = useCallback(() => { const nextSelectedIndex = 0; (describeReq as any).cache = new WeakMap(); setViewingReq(requests[nextSelectedIndex]); selectedReq.current = requests[nextSelectedIndex]; setSelectedIndex(nextSelectedIndex); scrollIntoView(nextSelectedIndex); - }; + }, [describeReq, requests, scrollIntoView]); const clear = () => { clearHistory(); initialize(); }; - const restore = (req: any = selectedReq.current) => { - dispatch({ type: 'restoreRequest', value: req }); - }; + const restoreRequestFromHistory = useRestoreRequestFromHistory(); useEffect(() => { initialize(); - }, [requests]); + }, [initialize]); useEffect(() => { const done = history.change(setPastRequests); return () => done(); - }, []); + }, [history]); /* eslint-disable */ return ( @@ -128,7 +126,7 @@ export function ConsoleHistory({ close }: Props) { ref={listRef} onKeyDown={(ev: React.KeyboardEvent) => { if (ev.keyCode === keyCodes.ENTER) { - restore(); + restoreRequestFromHistory(selectedReq.current); return; } @@ -173,12 +171,11 @@ export function ConsoleHistory({ close }: Props) { setViewingReq(req); selectedReq.current = req; setSelectedIndex(idx); - scrollIntoView(idx); }} role="option" onMouseEnter={() => setViewingReq(req)} onMouseLeave={() => setViewingReq(selectedReq.current)} - onDoubleClick={() => restore(req)} + onDoubleClick={restoreRequestFromHistory} aria-label={i18n.translate('console.historyPage.itemOfRequestListAriaLabel', { defaultMessage: 'Request: {historyItem}', values: { historyItem: reqDescription }, @@ -196,10 +193,7 @@ export function ConsoleHistory({ close }: Props) {
- +
@@ -224,7 +218,11 @@ export function ConsoleHistory({ close }: Props) { - restore()}> + restoreRequestFromHistory(selectedReq.current)} + > {i18n.translate('console.historyPage.applyHistoryButtonLabel', { defaultMessage: 'Apply', })} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx similarity index 86% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx index c15bec0563049..6fbb46bba6212 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx @@ -21,12 +21,12 @@ import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import $ from 'jquery'; -import { DevToolsSettings } from '../../../../../services'; -import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; +import { DevToolsSettings } from '../../../services'; +import { subscribeResizeChecker } from '../editor/legacy/subscribe_console_resize_checker'; // @ts-ignore -import SenseEditor from '../../../../../../../public/quarantined/src/sense_editor/editor'; -import { applyCurrentSettings } from '../console_editor/apply_editor_settings'; +import SenseEditor from '../../../../../public/quarantined/src/sense_editor/editor'; +import { applyCurrentSettings } from '../editor/legacy/console_editor/apply_editor_settings'; interface Props { settings: DevToolsSettings; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx deleted file mode 100644 index aa04a5ff3dd96..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx +++ /dev/null @@ -1,68 +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, { createContext, Dispatch, useContext, useReducer } from 'react'; -import { Action, reducer } from './reducer'; -import { DevToolsSettings } from '../../../../services'; - -export interface ContextValue { - editorsReady: boolean; - settings: DevToolsSettings; -} - -const EditorReadContext = createContext(null as any); -const EditorActionContext = createContext>(null as any); - -export interface EditorContextArgs { - children: any; - settings: DevToolsSettings; -} - -const initialValue: ContextValue = { - editorsReady: false, - settings: null as any, -}; - -export function EditorContextProvider({ children, settings }: EditorContextArgs) { - const [state, dispatch] = useReducer(reducer, initialValue, value => ({ - ...value, - settings, - })); - return ( - - {children} - - ); -} - -export const useEditorActionContext = () => { - const context = useContext(EditorActionContext); - if (context === undefined) { - throw new Error('useEditorActionContext must be used inside EditorActionContext'); - } - return context; -}; - -export const useEditorReadContext = () => { - const context = useContext(EditorReadContext); - if (context === undefined) { - throw new Error('useEditorReadContext must be used inside EditorContextProvider'); - } - return context; -}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts deleted file mode 100644 index caed6b24c3c11..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts +++ /dev/null @@ -1,77 +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 { Reducer } from 'react'; - -import { instance as registry } from './editor_registry'; -import { ContextValue } from './editor_context'; - -import { restoreRequestFromHistory } from '../legacy/console_history/restore_request_from_history'; -import { - sendCurrentRequestToES, - EsRequestArgs, -} from '../legacy/console_editor/send_current_request_to_es'; -import { DevToolsSettings } from '../../../../services'; - -export type Action = - | { type: 'setInputEditor'; value: any } - | { type: 'setOutputEditor'; value: any } - | { type: 'restoreRequest'; value: any } - | { type: 'updateSettings'; value: DevToolsSettings } - | { type: 'sendRequestToEs'; value: EsRequestArgs } - | { type: 'updateRequestHistory'; value: any }; - -export const reducer: Reducer = (state, action) => { - const nextState = { ...state }; - - if (action.type === 'setInputEditor') { - registry.setInputEditor(action.value); - if (registry.getOutputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'setOutputEditor') { - registry.setOutputEditor(action.value); - if (registry.getInputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'restoreRequest') { - restoreRequestFromHistory(registry.getInputEditor(), action.value); - } - - if (action.type === 'updateSettings') { - nextState.settings = action.value; - } - - if (action.type === 'sendRequestToEs') { - const { callback, isPolling, isUsingTripleQuotes } = action.value; - sendCurrentRequestToES({ - input: registry.getInputEditor(), - output: registry.getOutputEditor(), - callback, - isUsingTripleQuotes, - isPolling, - }); - } - - return nextState; -}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx new file mode 100644 index 0000000000000..07b48c083bf61 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useCallback } from 'react'; +import { debounce } from 'lodash'; + +import { Panel, PanelsContainer } from '../../components/split_panel'; +import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; +import { StorageKeys } from '../../../services'; +import { useServicesContext } from '../../contexts'; + +const INITIAL_PANEL_WIDTH = 50; +const PANEL_MIN_WIDTH = '100px'; + +export const Editor = () => { + const { + services: { storage }, + } = useServicesContext(); + + const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ + INITIAL_PANEL_WIDTH, + INITIAL_PANEL_WIDTH, + ]); + + const onPanelWidthChange = useCallback( + debounce((widths: number[]) => { + storage.set(StorageKeys.WIDTH, widths); + }, 300), + [] + ); + + return ( + + + + + + + + + ); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts index b3cab3d13b3a3..87436d7f97389 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { Editor, EditorOutput, ConsoleHistory, autoIndent, getDocumentation } from './legacy'; -export { useEditorActionContext, useEditorReadContext, EditorContextProvider } from './context'; +export { autoIndent, getDocumentation } from './legacy'; +export { Editor } from './editor'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx index 03d5b3f1d8f44..cb5559edfb249 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -20,15 +20,29 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; +import { act } from 'react-dom/test-utils'; import * as sinon from 'sinon'; -import { EditorContextProvider } from '../../context'; -import { AppContextProvider } from '../../../../context'; +import { + ServicesContextProvider, + EditorContextProvider, + RequestContextProvider, +} from '../../../../contexts'; + import { Editor } from './editor'; +jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ + instance: { + setInputEditor: () => {}, + getInputEditor: () => ({ + getRequestsInRange: (cb: any) => cb([{ test: 'test' }]), + }), + }, +})); jest.mock('../../../../components/editor_example.tsx', () => {}); jest.mock('../../../../../../../public/quarantined/src/mappings.js', () => ({ retrieveAutoCompleteInfo: () => {}, + clearSubscriptions: () => {}, })); jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { return { @@ -46,7 +60,7 @@ jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { }; }); -import * as sendRequestModule from './send_current_request_to_es'; +import * as sendRequestModule from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; import * as consoleMenuActions from '../console_menu_actions'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { @@ -66,19 +80,24 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }; editor = mount( - - - - - + + + + + + + ); }); - it('calls send current request to ES', () => { - const stub = sinon.stub(sendRequestModule, 'sendCurrentRequestToES'); + // TODO: Re-enable when React ^16.9 is available + it.skip('calls send current request to ES', () => { + const stub = sinon.stub(sendRequestModule, 'sendRequestToES'); try { - editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + act(() => { + editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + }); expect(stub.called).toBe(true); expect(stub.callCount).toBe(1); } finally { diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 10f1ef34602a6..0fa0ec732c770 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import $ from 'jquery'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext, useEditorReadContext } from '../../../../contexts'; import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode'; import { ConsoleMenu } from '../../../../components'; @@ -32,12 +32,13 @@ import { autoIndent, getDocumentation } from '../console_menu_actions'; import { registerCommands } from './keyboard_shortcuts'; import { applyCurrentSettings } from './apply_editor_settings'; +import { useSendCurrentRequestToES, useSetInputEditor } from '../../../../hooks'; + // @ts-ignore import { initializeEditor } from '../../../../../../../public/quarantined/src/input'; // @ts-ignore import mappings from '../../../../../../../public/quarantined/src/mappings'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { loadRemoteState } from './load_remote_editor_state'; @@ -60,14 +61,15 @@ const DEFAULT_INPUT_VALUE = `GET _search } }`; -function _Editor({ previousStateLocation = 'stored' }: EditorProps) { +function EditorUI({ previousStateLocation = 'stored' }: EditorProps) { const { services: { history, notifications }, docLinkVersion, - } = useAppContext(); + } = useServicesContext(); const { settings } = useEditorReadContext(); - const dispatch = useEditorActionContext(); + const setInputEditor = useSetInputEditor(); + const sendCurrentRequestToES = useSendCurrentRequestToES(); const editorRef = useRef(null); const actionsRef = useRef(null); @@ -76,13 +78,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { const [textArea, setTextArea] = useState(null); useUIAceKeyboardMode(textArea); - const openDocumentation = async () => { + const openDocumentation = useCallback(async () => { const documentation = await getDocumentation(editorInstanceRef.current!, docLinkVersion); if (!documentation) { return; } window.open(documentation, '_blank'); - }; + }, [docLinkVersion]); useEffect(() => { const $editor = $(editorRef.current!); @@ -102,7 +104,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { let timer: number; const saveDelay = 500; - return editorInstanceRef.current.getSession().on('change', function onChange() { + editorInstanceRef.current.getSession().on('change', function onChange() { if (timer) { clearTimeout(timer); } @@ -119,11 +121,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { } } - dispatch({ - type: 'setInputEditor', - value: editorInstanceRef.current, - }); - + setInputEditor(editorInstanceRef.current); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); @@ -132,26 +130,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { editorRef.current!, editorInstanceRef.current ); - const unsubscribeAutoSave = setupAutosave(); + setupAutosave(); return () => { unsubscribeResizer(); - unsubscribeAutoSave(); mappings.clearSubscriptions(); }; - }, []); - - const sendCurrentRequestToES = useCallback(() => { - dispatch({ - type: 'sendRequestToEs', - value: { - isUsingTripleQuotes: settings.tripleQuotes, - isPolling: settings.polling, - callback: (esPath: any, esMethod: any, esData: any) => - history.addToHistory(esPath, esMethod, esData), - }, - }); - }, [settings]); + }, [history, previousStateLocation, setInputEditor]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current!, settings); @@ -165,7 +150,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { sendCurrentRequestToES, openDocumentation, }); - }, [sendCurrentRequestToES]); + }, [sendCurrentRequestToES, openDocumentation]); return (
@@ -219,4 +204,4 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { ); } -export const Editor = React.memo(_Editor); +export const Editor = React.memo(EditorUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx index d38e86df41464..c167155bd18a9 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -16,39 +16,70 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useEffect, useRef } from 'react'; import $ from 'jquery'; // @ts-ignore import { initializeOutput } from '../../../../../../../public/quarantined/src/output'; -import { useAppContext } from '../../../../context'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { + useServicesContext, + useEditorReadContext, + useRequestReadContext, +} from '../../../../contexts'; + +// @ts-ignore +import utils from '../../../../../../../public/quarantined/src/utils'; + import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { applyCurrentSettings } from './apply_editor_settings'; -function _EditorOuput() { +function modeForContentType(contentType: string) { + if (contentType.indexOf('application/json') >= 0) { + return 'ace/mode/json'; + } else if (contentType.indexOf('application/yaml') >= 0) { + return 'ace/mode/yaml'; + } + return 'ace/mode/text'; +} + +function EditorOutputUI() { const editorRef = useRef(null); const editorInstanceRef = useRef(null); - const { - services: { settings }, - } = useAppContext(); - - const dispatch = useEditorActionContext(); + const { services } = useServicesContext(); const { settings: readOnlySettings } = useEditorReadContext(); + const { + lastResult: { data, error }, + } = useRequestReadContext(); useEffect(() => { const editor$ = $(editorRef.current!); - editorInstanceRef.current = initializeOutput(editor$, settings); - editorInstanceRef.current.update(''); + editorInstanceRef.current = initializeOutput(editor$, services.settings); const unsubscribe = subscribeResizeChecker(editorRef.current!, editorInstanceRef.current); - dispatch({ type: 'setOutputEditor', value: editorInstanceRef.current }); - return () => { unsubscribe(); }; - }, []); + }, [services.settings]); + + useEffect(() => { + if (data) { + const mode = modeForContentType(data[0].response.contentType); + editorInstanceRef.current.session.setMode(mode); + editorInstanceRef.current.update( + data + .map(d => d.response.value) + .map(readOnlySettings.tripleQuotes ? utils.expandLiteralStrings : a => a) + .join('\n') + ); + } else if (error) { + editorInstanceRef.current.session.setMode(modeForContentType(error.contentType)); + editorInstanceRef.current.update(error.value); + } else { + editorInstanceRef.current.update(''); + } + }, [readOnlySettings, data, error]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current, readOnlySettings); @@ -61,4 +92,4 @@ function _EditorOuput() { ); } -export const EditorOutput = React.memo(_EditorOuput); +export const EditorOutput = React.memo(EditorOutputUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts deleted file mode 100644 index d3abf9c92f48e..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts +++ /dev/null @@ -1,182 +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. - */ - -// @ts-ignore -import mappings from '../../../../../../../public/quarantined/src/mappings'; -// @ts-ignore -import utils from '../../../../../../../public/quarantined/src/utils'; -// @ts-ignore -import * as es from '../../../../../../../public/quarantined/src/es'; - -export interface EsRequestArgs { - callback: (esPath: any, esMethod: any, esData: any) => void; - input?: any; - output?: any; - isPolling: boolean; - isUsingTripleQuotes: boolean; -} - -let CURRENT_REQ_ID = 0; -export function sendCurrentRequestToES({ - callback, - input, - output, - isPolling, - isUsingTripleQuotes, -}: EsRequestArgs) { - const reqId = ++CURRENT_REQ_ID; - - input.getRequestsInRange((requests: any) => { - if (reqId !== CURRENT_REQ_ID) { - return; - } - if (output) { - output.update(''); - } - - if (requests.length === 0) { - return; - } - - const isMultiRequest = requests.length > 1; - const finishChain = () => { - /* noop */ - }; - - let isFirstRequest = true; - - const sendNextRequest = () => { - if (reqId !== CURRENT_REQ_ID) { - return; - } - if (requests.length === 0) { - finishChain(); - return; - } - const req = requests.shift(); - const esPath = req.url; - const esMethod = req.method; - let esData = utils.collapseLiteralStrings(req.data.join('\n')); - if (esData) { - esData += '\n'; - } // append a new line for bulk requests. - - es.send(esMethod, esPath, esData).always( - (dataOrjqXHR: any, textStatus: string, jqXhrORerrorThrown: any) => { - if (reqId !== CURRENT_REQ_ID) { - return; - } - - const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; - - function modeForContentType(contentType: string) { - if (contentType.indexOf('text/plain') >= 0) { - return 'ace/mode/text'; - } else if (contentType.indexOf('application/yaml') >= 0) { - return 'ace/mode/yaml'; - } - return null; - } - - const isSuccess = - typeof xhr.status === 'number' && - // Things like DELETE index where the index is not there are OK. - ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); - - if (isSuccess) { - if (xhr.status !== 404 && isPolling) { - // If the user has submitted a request against ES, something in the fields, indices, aliases, - // or templates may have changed, so we'll need to update this data. Assume that if - // the user disables polling they're trying to optimize performance or otherwise - // preserve resources, so they won't want this request sent either. - mappings.retrieveAutoCompleteInfo(); - } - - let value = xhr.responseText; - const mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - - // Apply triple quotes to output. - if (isUsingTripleQuotes && mode === null) { - // assume json - auto pretty - try { - value = utils.expandLiteralStrings(value); - } catch (e) { - // nothing to do here - } - } - - const warnings = xhr.getResponseHeader('warning'); - if (warnings) { - const deprecationMessages = utils.extractDeprecationMessages(warnings); - value = deprecationMessages.join('\n') + '\n' + value; - } - - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; - } - - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } - - isFirstRequest = false; - // single request terminate via sendNextRequest as well - - callback(esPath, esMethod, esData); - sendNextRequest(); - } else { - let value; - let mode; - if (xhr.responseText) { - value = xhr.responseText; // ES error should be shown - mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - if (value[0] === '{') { - try { - value = JSON.stringify(JSON.parse(value), null, 2); - } catch (e) { - // nothing to do here - } - } - } else { - value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; - mode = 'ace/mode/text'; - } - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; - } - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } - finishChain(); - } - } - ); - }; - - sendNextRequest(); - }); -} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts index 134f3de42833b..832295d4eb00b 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts @@ -18,5 +18,4 @@ */ export { EditorOutput, Editor } from './console_editor'; -export { ConsoleHistory } from './console_history'; export { getDocumentation, autoIndent } from './console_menu_actions'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx index 269f4e2cdeb72..ca74b19b76f16 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx @@ -35,41 +35,40 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n const overlayMountNode = useRef(null); const autoCompleteVisibleRef = useRef(false); - function onDismissOverlay(event: KeyboardEvent) { - if (event.keyCode === keyCodes.ENTER) { - event.preventDefault(); - aceTextAreaElement!.focus(); - } - } - - function enableOverlay() { - if (overlayMountNode.current) { - overlayMountNode.current.focus(); + useEffect(() => { + function onDismissOverlay(event: KeyboardEvent) { + if (event.keyCode === keyCodes.ENTER) { + event.preventDefault(); + aceTextAreaElement!.focus(); + } } - } - const isAutoCompleteVisible = () => { - const autoCompleter = document.querySelector('.ace_autocomplete'); - if (!autoCompleter) { - return false; + function enableOverlay() { + if (overlayMountNode.current) { + overlayMountNode.current.focus(); + } } - // The autoComplete is just hidden when it's closed, not removed from the DOM. - return autoCompleter.style.display !== 'none'; - }; - const documentKeyDownListener = () => { - autoCompleteVisibleRef.current = isAutoCompleteVisible(); - }; + const isAutoCompleteVisible = () => { + const autoCompleter = document.querySelector('.ace_autocomplete'); + if (!autoCompleter) { + return false; + } + // The autoComplete is just hidden when it's closed, not removed from the DOM. + return autoCompleter.style.display !== 'none'; + }; - const aceKeydownListener = (event: KeyboardEvent) => { - if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { - event.preventDefault(); - event.stopPropagation(); - enableOverlay(); - } - }; + const documentKeyDownListener = () => { + autoCompleteVisibleRef.current = isAutoCompleteVisible(); + }; - useEffect(() => { + const aceKeydownListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { + event.preventDefault(); + event.stopPropagation(); + enableOverlay(); + } + }; if (aceTextAreaElement) { // We don't control HTML elements inside of ace so we imperatively create an element // that acts as a container and insert it just before ace's textarea element diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index 518630c5a07c1..764c4b8e87100 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,33 +17,25 @@ * under the License. */ -import React, { useCallback, useState } from 'react'; -import { debounce } from 'lodash'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; - -import { EditorOutput, Editor, ConsoleHistory } from '../editor'; +import { ConsoleHistory } from '../console_history'; +import { Editor } from '../editor'; import { Settings } from '../settings'; -// TODO: find out what this is: $(document.body).removeClass('fouc'); - -import { TopNavMenu, WelcomePanel, HelpPanel, PanelsContainer, Panel } from '../../components'; +import { TopNavMenu, WelcomePanel, HelpPanel } from '../../components'; -import { useAppContext } from '../../context'; -import { StorageKeys } from '../../../services'; +import { useServicesContext, useEditorReadContext } from '../../contexts'; import { getTopNavConfig } from './get_top_nav'; -import { useEditorReadContext } from '../editor'; - -const INITIAL_PANEL_WIDTH = 50; -const PANEL_MIN_WIDTH = '100px'; export function Main() { const { services: { storage }, - } = useAppContext(); + } = useServicesContext(); - const { editorsReady } = useEditorReadContext(); + const { ready: editorsReady } = useEditorReadContext(); const [showWelcome, setShowWelcomePanel] = useState( () => storage.get('version_welcome_shown') !== '@@SENSE_REVISION' @@ -53,18 +45,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ - INITIAL_PANEL_WIDTH, - INITIAL_PANEL_WIDTH, - ]); - - const onPanelWidthChange = useCallback( - debounce((widths: number[]) => { - storage.set(StorageKeys.WIDTH, widths); - }, 300), - [] - ); - const renderConsoleHistory = () => { return editorsReady ? setShowHistory(false)} /> : null; }; @@ -95,20 +75,7 @@ export function Main() { {showingHistory ? {renderConsoleHistory()} : null} - - - - - - - - + diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx index d794dc9302c25..8440faa6eeea8 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx @@ -22,9 +22,8 @@ import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; // @ts-ignore import mappings from '../../../../public/quarantined/src/mappings'; -import { useAppContext } from '../context'; +import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings } from '../../services'; -import { useEditorActionContext } from './editor/context'; const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToolsSettings) => { return Object.keys(newSettings.autocomplete).filter(key => { @@ -76,7 +75,7 @@ export interface Props { export function Settings({ onClose }: Props) { const { services: { settings }, - } = useAppContext(); + } = useServicesContext(); const dispatch = useEditorActionContext(); @@ -90,7 +89,7 @@ export function Settings({ onClose }: Props) { // Let the rest of the application know settings have updated. dispatch({ type: 'updateSettings', - value: newSettings, + payload: newSettings, }); onClose(); }; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx deleted file mode 100644 index be7aa87ac2894..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx +++ /dev/null @@ -1,51 +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, { createContext, useContext } from 'react'; -import { NotificationsSetup } from '../../../../../../../core/public'; -import { History, Storage, Settings } from '../../services'; - -interface ContextValue { - services: { - history: History; - storage: Storage; - settings: Settings; - notifications: NotificationsSetup; - }; - docLinkVersion: string; -} - -interface ContextProps { - value: ContextValue; - children: any; -} - -const AppContext = createContext(null as any); - -export function AppContextProvider({ children, value }: ContextProps) { - return {children}; -} - -export const useAppContext = () => { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error('useAppContext must be used inside the AppContextProvider.'); - } - return context; -}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts deleted file mode 100644 index 27d69f5736ffe..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export { useAppContext, AppContextProvider } from './app_context'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts new file mode 100644 index 0000000000000..03b93c7d5b8ba --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Context, useContext } from 'react'; + +export const createUseContext = (Ctx: Context, name: string) => { + return () => { + const ctx = useContext(Ctx); + if (!ctx) { + throw new Error(`${name} should be used inside of ${name}Provider!`); + } + return ctx; + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx new file mode 100644 index 0000000000000..d5ed44e3f6ba2 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx @@ -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 React, { createContext, Dispatch, useReducer } from 'react'; +import * as editor from '../../stores/editor'; +import { DevToolsSettings } from '../../../services'; +import { createUseContext } from '../create_use_context'; + +const EditorReadContext = createContext(null as any); +const EditorActionContext = createContext>(null as any); + +export interface EditorContextArgs { + children: any; + settings: DevToolsSettings; +} + +export function EditorContextProvider({ children, settings }: EditorContextArgs) { + const [state, dispatch] = useReducer(editor.reducer, editor.initialValue, value => ({ + ...value, + settings, + })); + return ( + + {children} + + ); +} + +export const useEditorReadContext = createUseContext(EditorReadContext, 'EditorReadContext'); +export const useEditorActionContext = createUseContext(EditorActionContext, 'EditorActionContext'); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts index 6f14c6fc84150..bdccc1af0860c 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts @@ -19,23 +19,14 @@ export class EditorRegistry { inputEditor: any; - outputEditor: any; setInputEditor(inputEditor: any) { this.inputEditor = inputEditor; } - setOutputEditor(outputEditor: any) { - this.outputEditor = outputEditor; - } - getInputEditor() { return this.inputEditor; } - - getOutputEditor() { - return this.outputEditor; - } } // Create a single instance of this and use as private state. diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts new file mode 100644 index 0000000000000..18234acf15957 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useServicesContext, ServicesContextProvider } from './services_context'; + +export { + useRequestActionContext, + useRequestReadContext, + RequestContextProvider, +} from './request_context'; + +export { + useEditorActionContext, + useEditorReadContext, + EditorContextProvider, +} from './editor_context'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx new file mode 100644 index 0000000000000..faaa3196a97bc --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx @@ -0,0 +1,40 @@ +/* + * 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, { createContext, useReducer, Dispatch } from 'react'; +import { createUseContext } from './create_use_context'; +import * as store from '../stores/request'; + +const RequestReadContext = createContext(null as any); +const RequestActionContext = createContext>(null as any); + +export function RequestContextProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(store.reducer, store.initialValue); + return ( + + {children} + + ); +} + +export const useRequestReadContext = createUseContext(RequestReadContext, 'RequestReadContext'); +export const useRequestActionContext = createUseContext( + RequestActionContext, + 'RequestActionContext' +); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx new file mode 100644 index 0000000000000..f715b1ae53a78 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx @@ -0,0 +1,51 @@ +/* + * 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, { createContext, useContext } from 'react'; +import { NotificationsSetup } from 'kibana/public'; +import { History, Storage, Settings } from '../../services'; + +interface ContextValue { + services: { + history: History; + storage: Storage; + settings: Settings; + notifications: NotificationsSetup; + }; + docLinkVersion: string; +} + +interface ContextProps { + value: ContextValue; + children: any; +} + +const ServicesContext = createContext(null as any); + +export function ServicesContextProvider({ children, value }: ContextProps) { + return {children}; +} + +export const useServicesContext = () => { + const context = useContext(ServicesContext); + if (context === undefined) { + throw new Error('useAppContext must be used inside the AppContextProvider.'); + } + return context; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts new file mode 100644 index 0000000000000..8c5a8d599a0df --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useSetInputEditor } from './use_set_input_editor'; +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts new file mode 100644 index 0000000000000..017344ae537ab --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts similarity index 90% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts index f7f691e083ca2..b053e605b5fae 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts @@ -17,6 +17,10 @@ * under the License. */ +/** + * This function is considered legacy and should not be changed or updated before we have editor + * interfaces in place (it's using a customized version of Ace directly). + */ export function restoreRequestFromHistory(input: any, req: any) { const session = input.getSession(); let pos = input.getCursorPosition(); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts new file mode 100644 index 0000000000000..590ad78e6c236 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts @@ -0,0 +1,28 @@ +/* + * 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 { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { restoreRequestFromHistory } from './restore_request_from_history'; + +export const useRestoreRequestFromHistory = () => { + return useCallback((req: any) => { + const editor = registry.getInputEditor(); + restoreRequestFromHistory(editor, req); + }, []); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts new file mode 100644 index 0000000000000..a8f59d573c1a0 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts new file mode 100644 index 0000000000000..22fa4477e9012 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ + +// @ts-ignore +import utils from '../../../../../public/quarantined/src/utils'; +// @ts-ignore +import * as es from '../../../../../public/quarantined/src/es'; +import { BaseResponseType } from '../../../types/common'; + +export interface EsRequestArgs { + requests: any; +} + +export interface ESRequestResult { + request: { + path: string; + data: any; + method: string; + }; + response: { + contentType: BaseResponseType; + value: unknown; + }; +} + +let CURRENT_REQ_ID = 0; +export function sendRequestToES({ requests }: EsRequestArgs): Promise { + return new Promise((resolve, reject) => { + const reqId = ++CURRENT_REQ_ID; + const results: ESRequestResult[] = []; + if (reqId !== CURRENT_REQ_ID) { + return; + } + + if (requests.length === 0) { + return; + } + + const isMultiRequest = requests.length > 1; + + const sendNextRequest = () => { + if (reqId !== CURRENT_REQ_ID) { + resolve(results); + return; + } + if (requests.length === 0) { + resolve(results); + return; + } + const req = requests.shift(); + const esPath = req.url; + const esMethod = req.method; + let esData = utils.collapseLiteralStrings(req.data.join('\n')); + if (esData) { + esData += '\n'; + } // append a new line for bulk requests. + + es.send(esMethod, esPath, esData).always( + (dataOrjqXHR: any, textStatus: string, jqXhrORerrorThrown: any) => { + if (reqId !== CURRENT_REQ_ID) { + return; + } + + const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; + + const isSuccess = + typeof xhr.status === 'number' && + // Things like DELETE index where the index is not there are OK. + ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); + + if (isSuccess) { + let value = xhr.responseText; + + const warnings = xhr.getResponseHeader('warning'); + if (warnings) { + const deprecationMessages = utils.extractDeprecationMessages(warnings); + value = deprecationMessages.join('\n') + '\n' + value; + } + + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; + } + + results.push({ + response: { + contentType: xhr.getResponseHeader('Content-Type'), + value, + }, + request: { + data: esData, + method: esMethod, + path: esPath, + }, + }); + + // single request terminate via sendNextRequest as well + sendNextRequest(); + } else { + let value; + let contentType: string; + if (xhr.responseText) { + value = xhr.responseText; // ES error should be shown + contentType = xhr.getResponseHeader('Content-Type'); + } else { + value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; + contentType = 'text/plain'; + } + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; + } + reject({ value, contentType }); + } + } + ); + }; + + sendNextRequest(); + }); +} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts new file mode 100644 index 0000000000000..63d1120808e02 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -0,0 +1,85 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { useServicesContext } from '../../contexts'; +import { sendRequestToES } from './send_request_to_es'; +import { useRequestActionContext } from '../../contexts'; +// @ts-ignore +import mappings from '../../../../../public/quarantined/src/mappings'; + +export const useSendCurrentRequestToES = () => { + const { + services: { history, settings, notifications }, + } = useServicesContext(); + + const dispatch = useRequestActionContext(); + + return useCallback(async () => { + dispatch({ type: 'sendRequest', payload: undefined }); + try { + const editor = registry.getInputEditor(); + const requests = await new Promise(resolve => editor.getRequestsInRange(resolve)); + if (!requests.length) { + dispatch({ + type: 'requestFail', + payload: { value: 'No requests in range', contentType: 'text/plain' }, + }); + return; + } + const results = await sendRequestToES({ + requests, + }); + + results.forEach(({ request: { path, method, data } }) => { + history.addToHistory(path, method, data); + }); + + const { polling } = settings.toJSON(); + if (polling) { + // If the user has submitted a request against ES, something in the fields, indices, aliases, + // or templates may have changed, so we'll need to update this data. Assume that if + // the user disables polling they're trying to optimize performance or otherwise + // preserve resources, so they won't want this request sent either. + mappings.retrieveAutoCompleteInfo(); + } + + dispatch({ + type: 'requestSuccess', + payload: { + data: results, + }, + }); + } catch (e) { + if (e.contentType) { + dispatch({ + type: 'requestFail', + payload: e, + }); + } else { + notifications.toasts.addError(e, { + title: i18n.translate('console.unknownRequestErrorTitle', { + defaultMessage: 'Unknown Request Error', + }), + }); + } + } + }, [dispatch, settings, history, notifications]); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts new file mode 100644 index 0000000000000..672f3e269ead9 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEditorActionContext } from '../contexts/editor_context'; +import { instance as registry } from '../contexts/editor_context/editor_registry'; + +export const useSetInputEditor = () => { + const dispatch = useEditorActionContext(); + + return (editor: any) => { + dispatch({ type: 'setInputEditor', payload: editor }); + registry.setInputEditor(editor); + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx index aaacfd3894d18..e181caf23d2cb 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx @@ -18,9 +18,8 @@ */ import React from 'react'; -import { NotificationsSetup } from '../../../../../../core/public'; -import { AppContextProvider } from './context'; -import { EditorContextProvider } from './containers/editor/context'; +import { NotificationsSetup } from 'src/core/public'; +import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; @@ -46,16 +45,18 @@ export function boot(deps: { return ( - - -
- - + + +
+ + + ); } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts new file mode 100644 index 0000000000000..339a2f7a2c4af --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts @@ -0,0 +1,57 @@ +/* + * 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 { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { DevToolsSettings } from '../../services'; + +export interface Store { + ready: boolean; + settings: DevToolsSettings; +} + +export const initialValue: Store = produce( + { + ready: false, + settings: null as any, + }, + identity +); + +export type Action = + | { type: 'setInputEditor'; payload: any } + | { type: 'updateSettings'; payload: DevToolsSettings }; + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'setInputEditor') { + if (action.payload) { + draft.ready = true; + } + return; + } + + if (action.type === 'updateSettings') { + draft.settings = action.payload; + return; + } + + return draft; + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts new file mode 100644 index 0000000000000..fec7f4195eb74 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { BaseResponseType } from '../../types/common'; +import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; + +export type Actions = + | { type: 'sendRequest'; payload: undefined } + | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } + | { type: 'requestFail'; payload: { contentType: BaseResponseType; value: string } }; + +export interface Store { + requestInFlight: boolean; + lastResult: { + data: ESRequestResult[] | null; + error?: { contentType: BaseResponseType; value: string }; + }; +} + +const initialResultValue = { + data: null, + type: 'unknown' as BaseResponseType, +}; + +export const initialValue: Store = produce( + { + requestInFlight: false, + lastResult: initialResultValue, + }, + identity +); + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'sendRequest') { + draft.requestInFlight = true; + draft.lastResult = initialResultValue; + return; + } + + if (action.type === 'requestSuccess') { + draft.requestInFlight = false; + draft.lastResult = action.payload; + return; + } + + if (action.type === 'requestFail') { + draft.requestInFlight = false; + draft.lastResult = { ...initialResultValue, error: action.payload }; + return; + } + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/legacy.ts b/src/legacy/core_plugins/console/np_ready/public/legacy.ts index 463aac74da944..758ea81be88ad 100644 --- a/src/legacy/core_plugins/console/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/console/np_ready/public/legacy.ts @@ -29,8 +29,8 @@ import { I18nContext } from 'ui/i18n'; /* eslint-enable @kbn/eslint/no-restricted-paths */ export interface XPluginSet { - devTools: DevToolsSetup; - feature_catalogue: FeatureCatalogueSetup; + dev_tools: DevToolsSetup; + home: HomePublicPluginSetup; __LEGACY: { I18nContext: any; }; @@ -38,7 +38,7 @@ export interface XPluginSet { import { plugin } from '.'; import { DevToolsSetup } from '../../../../../plugins/dev_tools/public'; -import { FeatureCatalogueSetup } from '../../../../../plugins/feature_catalogue/public'; +import { HomePublicPluginSetup } from '../../../../../plugins/home/public'; const pluginInstance = plugin({} as any); diff --git a/src/legacy/core_plugins/console/np_ready/public/plugin.ts b/src/legacy/core_plugins/console/np_ready/public/plugin.ts index 301b85b6e7395..37758adc98d11 100644 --- a/src/legacy/core_plugins/console/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/console/np_ready/public/plugin.ts @@ -31,11 +31,11 @@ export class ConsoleUIPlugin implements Plugin { async setup({ notifications }: CoreSetup, pluginSet: XPluginSet) { const { __LEGACY: { I18nContext }, - devTools, - feature_catalogue, + dev_tools, + home, } = pluginSet; - feature_catalogue.register({ + home.featureCatalogue.register({ id: 'console', title: i18n.translate('console.devToolsTitle', { defaultMessage: 'Console', @@ -49,7 +49,7 @@ export class ConsoleUIPlugin implements Plugin { category: FeatureCatalogueCategory.ADMIN, }); - devTools.register({ + dev_tools.register({ id: 'console', order: 1, title: i18n.translate('console.consoleDisplayName', { diff --git a/src/legacy/core_plugins/console/np_ready/public/types/common.ts b/src/legacy/core_plugins/console/np_ready/public/types/common.ts new file mode 100644 index 0000000000000..ad9ed10d4188f --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/types/common.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type BaseResponseType = + | 'application/json' + | 'text/csv' + | 'text/tab-separated-values' + | 'text/plain' + | 'application/yaml' + | 'unknown'; diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 1e13b6b483981..b19fd438f8ee3 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,5 +1,8 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). +@import '@elastic/eui/src/components/header/variables'; + #consoleRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); display: flex; flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js index e6cae3f1710bc..0c00b2f93ee6f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js +++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js @@ -21,7 +21,7 @@ import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent); + super(name, mappings.getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js index e54bc2476698a..c2596dc4258da 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js @@ -352,6 +352,13 @@ const rules = { }, missing: '', }, + cumulative_cardinality: { + __template: { + buckets_path: '', + }, + buckets_path: '', + format: '', + }, scripted_metric: { __template: { init_script: '', diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json index cc7218be2e48d..c19836e2f9eb0 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json @@ -1,10 +1,16 @@ { "indices.put_template": { "data_autocomplete_rules": { - "template": "index*", - "warmers": { "__scope_link": "_warmer" }, + "index_patterns": [], "mappings": { "__scope_link": "put_mapping" }, - "settings": { "__scope_link": "put_settings" } + "settings": { "__scope_link": "put_settings" }, + "version": 0, + "order": 0, + "aliases": { + "__template": { + "NAME": {} + } + } }, "patterns": [ "_template/{template}" diff --git a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts index 39ec1f78b65f0..946b3997a9712 100644 --- a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts +++ b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts @@ -29,10 +29,10 @@ import { esFilters, FilterManager, TimefilterContract, + applyFiltersPopover, changeTimeFilter, extractTimeFilter, } from '../../../../../../plugins/data/public'; -import { applyFiltersPopover } from '../apply_filters/apply_filters_popover'; import { IndexPatternsStart } from '../../index_patterns'; export const GLOBAL_APPLY_FILTER_ACTION = 'GLOBAL_APPLY_FILTER_ACTION'; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx deleted file mode 100644 index 41f757e726c40..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx +++ /dev/null @@ -1,74 +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 { EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React, { Component } from 'react'; -import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; -import { IndexPattern } from '../../index_patterns/index_patterns'; -import { esFilters } from '../../../../../../plugins/data/public'; - -interface Props { - filters: esFilters.Filter[]; - onCancel: () => void; - onSubmit: (filters: esFilters.Filter[]) => void; - indexPatterns: IndexPattern[]; -} - -interface State { - isFilterSelected: boolean[]; -} - -export class ApplyFiltersPopover extends Component { - public render() { - if (!this.props.filters || this.props.filters.length === 0) { - return ''; - } - - return ( - - - - - - ); - } -} - -type cancelFunction = () => void; -type submitFunction = (filters: esFilters.Filter[]) => void; -export const applyFiltersPopover = ( - filters: esFilters.Filter[], - indexPatterns: IndexPattern[], - onCancel: cancelFunction, - onSubmit: submitFunction -) => { - return ( - - ); -}; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts deleted file mode 100644 index 6b64230ed6a0c..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export { ApplyFiltersPopover } from './apply_filters_popover'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx deleted file mode 100644 index 5b389f5b98aba..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ /dev/null @@ -1,239 +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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import classNames from 'classnames'; -import React, { useState } from 'react'; -import { CoreStart } from 'src/core/public'; -import { IndexPattern } from '../../index_patterns'; -import { FilterEditor } from './filter_editor'; -import { FilterItem } from './filter_item'; -import { FilterOptions } from './filter_options'; -import { useKibana, KibanaContextProvider } from '../../../../../../plugins/kibana_react/public'; -import { DataPublicPluginStart, esFilters } from '../../../../../../plugins/data/public'; - -interface Props { - filters: esFilters.Filter[]; - onFiltersUpdated?: (filters: esFilters.Filter[]) => void; - className: string; - indexPatterns: IndexPattern[]; - intl: InjectedIntl; - - // TODO: Only for filter-bar directive! - uiSettings?: CoreStart['uiSettings']; - docLinks?: CoreStart['docLinks']; - pluginDataStart?: DataPublicPluginStart; -} - -function FilterBarUI(props: Props) { - const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const kibana = useKibana(); - - const uiSettings = kibana.services.uiSettings || props.uiSettings; - if (!uiSettings) return null; - - function hasContext() { - return Boolean(kibana.services.uiSettings); - } - - function wrapInContextIfMissing(content: JSX.Element) { - // TODO: Relevant only as long as directives are used! - if (!hasContext()) { - if (props.docLinks && props.uiSettings && props.pluginDataStart) { - return ( - - {content} - - ); - } else { - throw new Error( - 'Rending filter bar requires providing sufficient context: uiSettings, docLinks and NP data plugin' - ); - } - } - return content; - } - - function onFiltersUpdated(filters: esFilters.Filter[]) { - if (props.onFiltersUpdated) { - props.onFiltersUpdated(filters); - } - } - - function renderItems() { - return props.filters.map((filter, i) => ( - - onUpdate(i, newFilter)} - onRemove={() => onRemove(i)} - indexPatterns={props.indexPatterns} - uiSettings={uiSettings!} - /> - - )); - } - - function renderAddFilter() { - const isPinned = uiSettings!.get('filters:pinnedByDefault'); - const [indexPattern] = props.indexPatterns; - const index = indexPattern && indexPattern.id; - const newFilter = esFilters.buildEmptyFilter(isPinned, index); - - const button = ( - setIsAddFilterPopoverOpen(true)} - data-test-subj="addFilter" - className="globalFilterBar__addButton" - > - +{' '} - - - ); - - return wrapInContextIfMissing( - - setIsAddFilterPopoverOpen(false)} - anchorPosition="downLeft" - withTitle - panelPaddingSize="none" - ownFocus={true} - > - -
- setIsAddFilterPopoverOpen(false)} - key={JSON.stringify(newFilter)} - /> -
-
-
-
- ); - } - - function onAdd(filter: esFilters.Filter) { - setIsAddFilterPopoverOpen(false); - const filters = [...props.filters, filter]; - onFiltersUpdated(filters); - } - - function onRemove(i: number) { - const filters = [...props.filters]; - filters.splice(i, 1); - onFiltersUpdated(filters); - } - - function onUpdate(i: number, filter: esFilters.Filter) { - const filters = [...props.filters]; - filters[i] = filter; - onFiltersUpdated(filters); - } - - function onEnableAll() { - const filters = props.filters.map(esFilters.enableFilter); - onFiltersUpdated(filters); - } - - function onDisableAll() { - const filters = props.filters.map(esFilters.disableFilter); - onFiltersUpdated(filters); - } - - function onPinAll() { - const filters = props.filters.map(esFilters.pinFilter); - onFiltersUpdated(filters); - } - - function onUnpinAll() { - const filters = props.filters.map(esFilters.unpinFilter); - onFiltersUpdated(filters); - } - - function onToggleAllNegated() { - const filters = props.filters.map(esFilters.toggleFilterNegated); - onFiltersUpdated(filters); - } - - function onToggleAllDisabled() { - const filters = props.filters.map(esFilters.toggleFilterDisabled); - onFiltersUpdated(filters); - } - - function onRemoveAll() { - onFiltersUpdated([]); - } - - const classes = classNames('globalFilterBar', props.className); - - return ( - - - - - - - - {renderItems()} - {renderAddFilter()} - - - - ); -} - -export const FilterBar = injectI18n(FilterBarUI); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx deleted file mode 100644 index 84da576e8205c..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ /dev/null @@ -1,507 +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 { - EuiButton, - EuiButtonEmpty, - // @ts-ignore - EuiCodeEditor, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiSwitchEvent, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { Component } from 'react'; -import { Field, IndexPattern } from '../../../index_patterns'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; -import { - buildCustomFilter, - buildFilter, - getFieldFromFilter, - getFilterableFields, - getFilterParams, - getIndexPatternFromFilter, - getOperatorFromFilter, - getOperatorOptions, - getQueryDslFromFilter, - isFilterValid, -} from './lib/filter_editor_utils'; -import { Operator } from './lib/filter_operators'; -import { PhraseValueInput } from './phrase_value_input'; -import { PhrasesValuesInput } from './phrases_values_input'; -import { RangeValueInput } from './range_value_input'; -import { esFilters } from '../../../../../../../plugins/data/public'; - -interface Props { - filter: esFilters.Filter; - indexPatterns: IndexPattern[]; - onSubmit: (filter: esFilters.Filter) => void; - onCancel: () => void; - intl: InjectedIntl; -} - -interface State { - selectedIndexPattern?: IndexPattern; - selectedField?: Field; - selectedOperator?: Operator; - params: any; - useCustomLabel: boolean; - customLabel: string | null; - queryDsl: string; - isCustomEditorOpen: boolean; -} - -class FilterEditorUI extends Component { - constructor(props: Props) { - super(props); - this.state = { - selectedIndexPattern: this.getIndexPatternFromFilter(), - selectedField: this.getFieldFromFilter(), - selectedOperator: this.getSelectedOperator(), - params: getFilterParams(props.filter), - useCustomLabel: props.filter.meta.alias !== null, - customLabel: props.filter.meta.alias, - queryDsl: JSON.stringify(getQueryDslFromFilter(props.filter), null, 2), - isCustomEditorOpen: this.isUnknownFilterType(), - }; - } - - public render() { - return ( -
- - - - - - - - {this.state.isCustomEditorOpen ? ( - - ) : ( - - )} - - - - - -
- - {this.renderIndexPatternInput()} - - {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} - - - - - - {this.state.useCustomLabel && ( -
- - - - -
- )} - - - - - - - - - - - - - - - - -
-
-
- ); - } - - private renderIndexPatternInput() { - if ( - this.props.indexPatterns.length <= 1 && - this.props.indexPatterns.find( - indexPattern => indexPattern === this.state.selectedIndexPattern - ) - ) { - return ''; - } - const { selectedIndexPattern } = this.state; - return ( - - - - indexPattern.title} - onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterIndexPatternsSelect" - /> - - - - ); - } - - private renderRegularEditor() { - return ( -
- - {this.renderFieldInput()} - - {this.renderOperatorInput()} - - - -
{this.renderParamsEditor()}
-
- ); - } - - private renderFieldInput() { - const { selectedIndexPattern, selectedField } = this.state; - const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; - - return ( - - field.name} - onChange={this.onFieldChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - className="globalFilterEditor__fieldInput" - data-test-subj="filterFieldSuggestionList" - /> - - ); - } - - private renderOperatorInput() { - const { selectedField, selectedOperator } = this.state; - const operators = selectedField ? getOperatorOptions(selectedField) : []; - return ( - - message} - onChange={this.onOperatorChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterOperatorList" - /> - - ); - } - - private renderCustomEditor() { - return ( - - - - ); - } - - private renderParamsEditor() { - const indexPattern = this.state.selectedIndexPattern; - if (!indexPattern || !this.state.selectedOperator) { - return ''; - } - - switch (this.state.selectedOperator.type) { - case 'exists': - return ''; - case 'phrase': - return ( - - ); - case 'phrases': - return ( - - ); - case 'range': - return ( - - ); - } - } - - private toggleCustomEditor = () => { - const isCustomEditorOpen = !this.state.isCustomEditorOpen; - this.setState({ isCustomEditorOpen }); - }; - - private isUnknownFilterType() { - const { type } = this.props.filter.meta; - return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); - } - - private getIndexPatternFromFilter() { - return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); - } - - private getFieldFromFilter() { - const indexPattern = this.getIndexPatternFromFilter(); - return ( - indexPattern && getFieldFromFilter(this.props.filter as esFilters.FieldFilter, indexPattern) - ); - } - - private getSelectedOperator() { - return getOperatorFromFilter(this.props.filter); - } - - private isFilterValid() { - const { - isCustomEditorOpen, - queryDsl, - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - } = this.state; - - if (isCustomEditorOpen) { - try { - return Boolean(JSON.parse(queryDsl)); - } catch (e) { - return false; - } - } - - return isFilterValid(indexPattern, field, operator, params); - } - - private onIndexPatternChange = ([selectedIndexPattern]: IndexPattern[]) => { - const selectedField = undefined; - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); - }; - - private onFieldChange = ([selectedField]: Field[]) => { - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedField, selectedOperator, params }); - }; - - private onOperatorChange = ([selectedOperator]: Operator[]) => { - // Only reset params when the operator type changes - const params = - get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') - ? this.state.params - : undefined; - this.setState({ selectedOperator, params }); - }; - - private onCustomLabelSwitchChange = (event: EuiSwitchEvent) => { - const useCustomLabel = event.target.checked; - const customLabel = event.target.checked ? '' : null; - this.setState({ useCustomLabel, customLabel }); - }; - - private onCustomLabelChange = (event: React.ChangeEvent) => { - const customLabel = event.target.value; - this.setState({ customLabel }); - }; - - private onParamsChange = (params: any) => { - this.setState({ params }); - }; - - private onQueryDslChange = (queryDsl: string) => { - this.setState({ queryDsl }); - }; - - private onSubmit = () => { - const { - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - useCustomLabel, - customLabel, - isCustomEditorOpen, - queryDsl, - } = this.state; - - const { $state } = this.props.filter; - if (!$state || !$state.store) { - return; // typescript validation - } - const alias = useCustomLabel ? customLabel : null; - - if (isCustomEditorOpen) { - const { index, disabled, negate } = this.props.filter.meta; - const newIndex = index || this.props.indexPatterns[0].id!; - const body = JSON.parse(queryDsl); - const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, $state.store); - this.props.onSubmit(filter); - } else if (indexPattern && field && operator) { - const filter = buildFilter( - indexPattern, - field, - operator, - this.props.filter.meta.disabled, - params, - alias, - $state.store - ); - this.props.onSubmit(filter); - } - }; -} - -function IndexPatternComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function FieldComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function OperatorComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const FilterEditor = injectI18n(FilterEditorUI); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts deleted file mode 100644 index 7ee3e375c0967..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ /dev/null @@ -1,370 +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. - */ - -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { stubIndexPattern, stubFields } from '../../../../../../../../plugins/data/public/stubs'; -import { IndexPattern, Field } from '../../../../index'; -import { - buildFilter, - getFieldFromFilter, - getFilterableFields, - getFilterParams, - getIndexPatternFromFilter, - getOperatorFromFilter, - getOperatorOptions, - getQueryDslFromFilter, - isFilterValid, -} from './filter_editor_utils'; -import { - doesNotExistOperator, - existsOperator, - isBetweenOperator, - isOneOfOperator, - isOperator, -} from './filter_operators'; -import { existsFilter } from './fixtures/exists_filter'; -import { phraseFilter } from './fixtures/phrase_filter'; -import { phrasesFilter } from './fixtures/phrases_filter'; -import { rangeFilter } from './fixtures/range_filter'; -import { esFilters } from '../../../../../../../../plugins/data/public'; - -jest.mock('ui/new_platform'); - -const mockedFields = stubFields as Field[]; -const mockedIndexPattern = stubIndexPattern as IndexPattern; - -describe('Filter editor utils', () => { - describe('getQueryDslFromFilter', () => { - it('should return query DSL without meta and $state', () => { - const queryDsl = getQueryDslFromFilter(phraseFilter); - expect(queryDsl).not.toHaveProperty('meta'); - expect(queryDsl).not.toHaveProperty('$state'); - }); - }); - - describe('getIndexPatternFromFilter', () => { - it('should return the index pattern from the filter', () => { - const indexPattern = getIndexPatternFromFilter(phraseFilter, [mockedIndexPattern]); - expect(indexPattern).toBe(mockedIndexPattern); - }); - }); - - describe('getFieldFromFilter', () => { - it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockedIndexPattern); - expect(field).not.toBeUndefined(); - expect(field && field.name).toBe(phraseFilter.meta.key); - }); - }); - - describe('getOperatorFromFilter', () => { - it('should return "is" for phrase filter', () => { - const operator = getOperatorFromFilter(phraseFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrase'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not" for phrase filter', () => { - const negatedPhraseFilter = esFilters.toggleFilterNegated(phraseFilter); - const operator = getOperatorFromFilter(negatedPhraseFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrase'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "is one of" for phrases filter', () => { - const operator = getOperatorFromFilter(phrasesFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrases'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not one of" for negated phrases filter', () => { - const negatedPhrasesFilter = esFilters.toggleFilterNegated(phrasesFilter); - const operator = getOperatorFromFilter(negatedPhrasesFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrases'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "is between" for range filter', () => { - const operator = getOperatorFromFilter(rangeFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('range'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not between" for negated range filter', () => { - const negatedRangeFilter = esFilters.toggleFilterNegated(rangeFilter); - const operator = getOperatorFromFilter(negatedRangeFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('range'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "exists" for exists filter', () => { - const operator = getOperatorFromFilter(existsFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('exists'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "does not exists" for negated exists filter', () => { - const negatedExistsFilter = esFilters.toggleFilterNegated(existsFilter); - const operator = getOperatorFromFilter(negatedExistsFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('exists'); - expect(operator && operator.negate).toBe(true); - }); - }); - - describe('getFilterParams', () => { - it('should retrieve params from phrase filter', () => { - const params = getFilterParams(phraseFilter); - expect(params).toBe('ios'); - }); - - it('should retrieve params from phrases filter', () => { - const params = getFilterParams(phrasesFilter); - expect(params).toEqual(['win xp', 'osx']); - }); - - it('should retrieve params from range filter', () => { - const params = getFilterParams(rangeFilter); - expect(params).toEqual({ from: 0, to: 10 }); - }); - - it('should return undefined for exists filter', () => { - const params = getFilterParams(existsFilter); - expect(params).toBeUndefined(); - }); - }); - - describe('getFilterableFields', () => { - it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); - expect(fieldOptions.length).toBeGreaterThan(0); - }); - - it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); - const nonFilterableFields = fieldOptions.filter(field => !field.filterable); - expect(nonFilterableFields.length).toBe(0); - }); - }); - - describe('getOperatorOptions', () => { - it('returns range for number fields', () => { - const [field] = stubFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field as Field); - const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); - expect(rangeOperator).not.toBeUndefined(); - }); - - it('does not return range for string fields', () => { - const [field] = stubFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field as Field); - const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); - expect(rangeOperator).toBeUndefined(); - }); - }); - - describe('isFilterValid', () => { - it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockedFields[0], isOperator, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, undefined, isOperator, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], undefined, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, []); - expect(isValid).toBe(false); - }); - - it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, ['foo']); - expect(isValid).toBe(true); - }); - - it('should return false for range filter without range', () => { - const isValid = isFilterValid( - mockedIndexPattern, - mockedFields[0], - isBetweenOperator, - undefined - ); - expect(isValid).toBe(false); - }); - - it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { - from: 'foo', - }); - expect(isValid).toBe(true); - }); - - it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { - from: 'foo', - too: 'goo', - }); - expect(isValid).toBe(true); - }); - - it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], existsOperator); - expect(isValid).toBe(true); - }); - }); - - describe('buildFilter', () => { - it('should build phrase filters', () => { - const params = 'foo'; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - isOperator, - false, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(isOperator.negate); - expect(filter.meta.alias).toBe(alias); - - expect(filter.$state).toBeDefined(); - if (filter.$state) { - expect(filter.$state.store).toBe(state); - } - }); - - it('should build phrases filters', () => { - const params = ['foo', 'bar']; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - isOneOfOperator, - false, - params, - alias, - state - ); - expect(filter.meta.type).toBe(isOneOfOperator.type); - expect(filter.meta.negate).toBe(isOneOfOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state).toBeDefined(); - if (filter.$state) { - expect(filter.$state.store).toBe(state); - } - }); - - it('should build range filters', () => { - const params = { from: 'foo', to: 'qux' }; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - isBetweenOperator, - false, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(isBetweenOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state).toBeDefined(); - if (filter.$state) { - expect(filter.$state.store).toBe(state); - } - }); - - it('should build exists filters', () => { - const params = undefined; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - existsOperator, - false, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(existsOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state).toBeDefined(); - if (filter.$state) { - expect(filter.$state.store).toBe(state); - } - }); - - it('should include disabled state', () => { - const params = undefined; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - doesNotExistOperator, - true, - params, - alias, - state - ); - expect(filter.meta.disabled).toBe(true); - }); - - it('should negate based on operator', () => { - const params = undefined; - const alias = 'bar'; - const state = esFilters.FilterStateStore.APP_STATE; - const filter = buildFilter( - mockedIndexPattern, - mockedFields[0], - doesNotExistOperator, - false, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(doesNotExistOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state).toBeDefined(); - if (filter.$state) { - expect(filter.$state.store).toBe(state); - } - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts deleted file mode 100644 index b7d20526a6b92..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ /dev/null @@ -1,167 +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 dateMath from '@elastic/datemath'; -import { omit } from 'lodash'; -import { Ipv4Address } from '../../../../../../../../plugins/kibana_utils/public'; -import { Field, IndexPattern, isFilterable } from '../../../../index_patterns'; -import { FILTER_OPERATORS, Operator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; - -export function getIndexPatternFromFilter( - filter: esFilters.Filter, - indexPatterns: IndexPattern[] -): IndexPattern | undefined { - return indexPatterns.find(indexPattern => indexPattern.id === filter.meta.index); -} - -export function getFieldFromFilter(filter: esFilters.FieldFilter, indexPattern: IndexPattern) { - return indexPattern.fields.find(field => field.name === filter.meta.key); -} - -export function getOperatorFromFilter(filter: esFilters.Filter) { - return FILTER_OPERATORS.find(operator => { - return filter.meta.type === operator.type && filter.meta.negate === operator.negate; - }); -} - -export function getQueryDslFromFilter(filter: esFilters.Filter) { - return omit(filter, ['$state', 'meta']); -} - -export function getFilterableFields(indexPattern: IndexPattern) { - return indexPattern.fields.filter(isFilterable); -} - -export function getOperatorOptions(field: Field) { - return FILTER_OPERATORS.filter(operator => { - return !operator.fieldTypes || operator.fieldTypes.includes(field.type); - }); -} - -export function getFilterParams(filter: esFilters.Filter) { - switch (filter.meta.type) { - case 'phrase': - return (filter as esFilters.PhraseFilter).meta.params.query; - case 'phrases': - return (filter as esFilters.PhrasesFilter).meta.params; - case 'range': - return { - from: (filter as esFilters.RangeFilter).meta.params.gte, - to: (filter as esFilters.RangeFilter).meta.params.lt, - }; - } -} - -export function validateParams(params: any, type: string) { - switch (type) { - case 'date': - const moment = typeof params === 'string' ? dateMath.parse(params) : null; - return Boolean(typeof params === 'string' && moment && moment.isValid()); - case 'ip': - try { - return Boolean(new Ipv4Address(params)); - } catch (e) { - return false; - } - default: - return true; - } -} - -export function isFilterValid( - indexPattern?: IndexPattern, - field?: Field, - operator?: Operator, - params?: any -) { - if (!indexPattern || !field || !operator) { - return false; - } - switch (operator.type) { - case 'phrase': - return validateParams(params, field.type); - case 'phrases': - if (!Array.isArray(params) || !params.length) { - return false; - } - return params.every(phrase => validateParams(phrase, field.type)); - case 'range': - if (typeof params !== 'object') { - return false; - } - return validateParams(params.from, field.type) || validateParams(params.to, field.type); - case 'exists': - return true; - default: - throw new Error(`Unknown operator type: ${operator.type}`); - } -} - -export function buildFilter( - indexPattern: IndexPattern, - field: Field, - operator: Operator, - disabled: boolean, - params: any, - alias: string | null, - store: esFilters.FilterStateStore -): esFilters.Filter { - const filter = buildBaseFilter(indexPattern, field, operator, params); - filter.meta.alias = alias; - filter.meta.negate = operator.negate; - filter.meta.disabled = disabled; - filter.$state = { store }; - return filter; -} - -function buildBaseFilter( - indexPattern: IndexPattern, - field: Field, - operator: Operator, - params: any -): esFilters.Filter { - switch (operator.type) { - case 'phrase': - return esFilters.buildPhraseFilter(field, params, indexPattern); - case 'phrases': - return esFilters.buildPhrasesFilter(field, params, indexPattern); - case 'range': - const newParams = { gte: params.from, lt: params.to }; - return esFilters.buildRangeFilter(field, newParams, indexPattern); - case 'exists': - return esFilters.buildExistsFilter(field, indexPattern); - default: - throw new Error(`Unknown operator type: ${operator.type}`); - } -} - -export function buildCustomFilter( - index: string, - queryDsl: any, - disabled: boolean, - negate: boolean, - alias: string | null, - store: esFilters.FilterStateStore -): esFilters.Filter { - const meta: esFilters.FilterMeta = { index, type: 'custom', disabled, negate, alias }; - const filter: esFilters.Filter = { ...queryDsl, meta }; - filter.$state = { store }; - return filter; -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_display_value.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_display_value.ts deleted file mode 100644 index d8af7b3e97ad2..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_display_value.ts +++ /dev/null @@ -1,48 +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 { get } from 'lodash'; -import { esFilters } from '../../../../../../../../plugins/data/public'; -import { IndexPattern } from '../../../../index_patterns/index_patterns'; -import { Field } from '../../../../index_patterns/fields'; -import { getIndexPatternFromFilter } from './filter_editor_utils'; - -function getValueFormatter(indexPattern?: IndexPattern, key?: string) { - if (!indexPattern || !key) return; - let format = get(indexPattern, ['fields', 'byName', key, 'format']); - if (!format && indexPattern.fields.getByName) { - // TODO: Why is indexPatterns sometimes a map and sometimes an array? - format = (indexPattern.fields.getByName(key) as Field).format; - } - return format; -} - -export function getDisplayValueFromFilter( - filter: esFilters.Filter, - indexPatterns: IndexPattern[] -): string { - const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); - - if (typeof filter.meta.value === 'function') { - const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key); - return filter.meta.value(valueFormatter); - } else { - return filter.meta.value || ''; - } -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx deleted file mode 100644 index fee043764d8d7..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx +++ /dev/null @@ -1,82 +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 { EuiBadge, useInnerText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { SFC } from 'react'; -import { FilterLabel } from '../filter_editor/lib/filter_label'; -import { esFilters } from '../../../../../../../plugins/data/public'; - -interface Props { - filter: esFilters.Filter; - valueLabel: string; - [propName: string]: any; -} - -export const FilterView: SFC = ({ - filter, - iconOnClick, - onClick, - valueLabel, - ...rest -}: Props) => { - const [ref, innerText] = useInnerText(); - - let title = i18n.translate('data.filter.filterBar.moreFilterActionsMessage', { - defaultMessage: 'Filter: {innerText}. Select for more filter actions.', - values: { innerText }, - }); - - if (esFilters.isFilterPinned(filter)) { - title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', { - defaultMessage: 'Pinned', - })} ${title}`; - } - if (filter.meta.disabled) { - title = `${i18n.translate('data.filter.filterBar.disabledFilterPrefix', { - defaultMessage: 'Disabled', - })} ${title}`; - } - - return ( - - - - - - ); -}; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts deleted file mode 100644 index 438d292b9f583..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export { FilterBar } from './filter_bar'; diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx deleted file mode 100644 index 005c4904a4f39..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ /dev/null @@ -1,22 +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. - */ - -export { FilterBar } from './filter_bar'; - -export { ApplyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 14274d27c13ee..913141666c7b9 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -2,6 +2,6 @@ @import './query/query_bar/index'; -@import './filter/filter_bar/index'; +@import 'src/plugins/data/public/ui/filter_bar/index'; @import './search/search_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 2412541e8c5c8..1349187779061 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { FilterBar, ApplyFiltersPopover } from './filter'; export { Field, FieldType, @@ -48,8 +47,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - isFilterable, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/core_plugins/data/public/index_patterns/fields/field.ts b/src/legacy/core_plugins/data/public/index_patterns/fields/field.ts index 6084b4c106452..91964655f6f3e 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/fields/field.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/fields/field.ts @@ -17,15 +17,13 @@ * under the License. */ -// @ts-ignore -import { fieldFormats } from 'ui/registry/field_formats'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { ObjDefine } from './obj_define'; // @ts-ignore import { shortenDottedString } from '../../../../../core_plugins/kibana/common/utils/shorten_dotted_string'; import { IndexPattern } from '../index_patterns'; -import { getNotifications } from '../services'; +import { getNotifications, getFieldFormats } from '../services'; import { FieldFormat, @@ -104,6 +102,8 @@ export class Field implements FieldType { let format = spec.format; if (!format || !(format instanceof FieldFormat)) { + const fieldFormats = getFieldFormats(); + format = indexPattern.fieldFormatMap[spec.name] || fieldFormats.getDefaultInstance(spec.type, spec.esTypes); diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index 2d43faf49f63d..ee9f9b493ebf2 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -27,17 +27,14 @@ import mockLogStashFields from '../../../../../../fixtures/logstash_fields'; import { stubbedSavedObjectIndexPattern } from '../../../../../../fixtures/stubbed_saved_object_index_pattern'; import { Field } from '../index_patterns_service'; -import { setNotifications } from '../services'; +import { setNotifications, setFieldFormats } from '../services'; // Temporary disable eslint, will be removed after moving to new platform folder // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { notificationServiceMock } from '../../../../../../core/public/notifications/notifications_service.mock'; +import { FieldFormatRegisty } from '../../../../../../plugins/data/public'; -jest.mock('ui/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn(), - }, -})); +jest.mock('ui/new_platform'); jest.mock('../../../../../../plugins/kibana_utils/public', () => { const originalModule = jest.requireActual('../../../../../../plugins/kibana_utils/public'); @@ -142,6 +139,9 @@ describe('IndexPattern', () => { // create an indexPattern instance for each test beforeEach(() => { setNotifications(notifications); + setFieldFormats(({ + getDefaultInstance: jest.fn(), + } as unknown) as FieldFormatRegisty); return create(indexPatternId).then((pattern: IndexPattern) => { indexPattern = pattern; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts index 12aa3c2fb0d51..f77342c7bc274 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -19,8 +19,6 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { fieldFormats } from 'ui/registry/field_formats'; import { SavedObjectsClientContract } from 'src/core/public'; import { DuplicateField, @@ -30,6 +28,12 @@ import { MappingObject, } from '../../../../../../plugins/kibana_utils/public'; +import { + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + IIndexPattern, +} from '../../../../../../plugins/data/public'; + import { findIndexPatternByTitle, getRoutes } from '../utils'; import { IndexPatternMissingIndices } from '../errors'; import { Field, FieldList, FieldListInterface, FieldType } from '../fields'; @@ -37,8 +41,7 @@ import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; import { IIndexPatternsApiClient } from './index_patterns_api_client'; -import { ES_FIELD_TYPES, IIndexPattern } from '../../../../../../plugins/data/public'; -import { getNotifications } from '../services'; +import { getNotifications, getFieldFormats } from '../services'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const type = 'index-pattern'; @@ -114,7 +117,10 @@ export class IndexPattern implements IIndexPattern { this.fields = new FieldList(this, [], this.shortDotsEnable); this.fieldsFetcher = createFieldsFetcher(this, apiClient, this.getConfig('metaFields')); this.flattenHit = flattenHitWrapper(this, this.getConfig('metaFields')); - this.formatHit = formatHitProvider(this, fieldFormats.getDefaultInstance('string')); + this.formatHit = formatHitProvider( + this, + getFieldFormats().getDefaultInstance(KBN_FIELD_TYPES.STRING) + ); this.formatField = this.formatHit.formatField; } @@ -125,12 +131,14 @@ export class IndexPattern implements IIndexPattern { } private deserializeFieldFormatMap(mapping: any) { - const FieldFormat = fieldFormats.getType(mapping.id); + const FieldFormat = getFieldFormats().getType(mapping.id); + return FieldFormat && new FieldFormat(mapping.params, this.getConfig); } private initFields(input?: any) { const newValue = input || this.fields; + this.fields = new FieldList(this, newValue, this.shortDotsEnable); } @@ -451,6 +459,7 @@ export class IndexPattern implements IIndexPattern { const { toasts } = getNotifications(); toasts.addDanger(message); + throw err; } @@ -492,6 +501,7 @@ export class IndexPattern implements IIndexPattern { if (err instanceof IndexPatternMissingIndices) { toasts.addDanger((err as any).message); + return []; } diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 8a5c78d13c251..0a5d1bfcae21f 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -29,12 +29,6 @@ jest.mock('../errors', () => ({ IndexPatternMissingIndices: jest.fn(), })); -jest.mock('ui/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn(), - }, -})); - jest.mock('./index_pattern', () => { class IndexPattern { init = async () => { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts index 2c58af9deaf49..c8e80b3aede20 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -24,8 +24,6 @@ import { UiSettingsClientContract, HttpServiceBase, } from 'src/core/public'; -// @ts-ignore -import { fieldFormats } from 'ui/registry/field_formats'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPattern } from './index_pattern'; @@ -34,8 +32,6 @@ import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_c const indexPatternCache = createIndexPatternCache(); export class IndexPatterns { - fieldFormats: fieldFormats; - private config: UiSettingsClientContract; private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts index 5dcf4005ef4e8..db1ece78e7b4d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts @@ -33,7 +33,6 @@ const createSetupContractMock = () => { flattenHitWrapper: jest.fn().mockImplementation(flattenHitWrapper), formatHitProvider: jest.fn(), indexPatterns: jest.fn() as any, - IndexPatternSelect: jest.fn(), __LEGACY: { // For BWC we must temporarily export the class implementation of Field, // which is only used externally by the Index Pattern UI. diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 9ce1b5f2e4a20..381cd491f0210 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -23,9 +23,9 @@ import { HttpServiceBase, NotificationsStart, } from 'src/core/public'; +import { FieldFormatsStart } from '../../../../../plugins/data/public'; import { Field, FieldList, FieldListInterface, FieldType } from './fields'; -import { createIndexPatternSelect } from './components'; -import { setNotifications } from './services'; +import { setNotifications, setFieldFormats } from './services'; import { createFlattenHitWrapper, @@ -40,6 +40,7 @@ export interface IndexPatternDependencies { savedObjectsClient: SavedObjectsClientContract; http: HttpServiceBase; notifications: NotificationsStart; + fieldFormats: FieldFormatsStart; } /** @@ -64,13 +65,19 @@ export class IndexPatternsService { return this.setupApi; } - public start({ uiSettings, savedObjectsClient, http, notifications }: IndexPatternDependencies) { + public start({ + uiSettings, + savedObjectsClient, + http, + notifications, + fieldFormats, + }: IndexPatternDependencies) { setNotifications(notifications); + setFieldFormats(fieldFormats); return { ...this.setupApi, indexPatterns: new IndexPatterns(uiSettings, savedObjectsClient, http), - IndexPatternSelect: createIndexPatternSelect(savedObjectsClient), }; } @@ -82,7 +89,6 @@ export class IndexPatternsService { // static code /** @public */ -export { IndexPatternSelect } from './components'; export { CONTAINS_SPACES, getFromSavedObject, @@ -90,7 +96,6 @@ export { ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - isFilterable, validateIndexPattern, } from './utils'; @@ -112,4 +117,4 @@ export type IndexPatternsStart = ReturnType; export { IndexPattern, IndexPatterns, StaticIndexPattern, Field, FieldType, FieldListInterface }; /** @public */ -export { getIndexPatternTitle, findIndexPatternByTitle } from './utils'; +export { findIndexPatternByTitle } from './utils'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/services.ts b/src/legacy/core_plugins/data/public/index_patterns/services.ts index 5cc087548d6fb..ecd898b28de63 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/services.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/services.ts @@ -19,7 +19,12 @@ import { NotificationsStart } from 'src/core/public'; import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; +import { FieldFormatsStart } from '../../../../../plugins/data/public'; export const [getNotifications, setNotifications] = createGetterSetter( 'Notifications' ); + +export const [getFieldFormats, setFieldFormats] = createGetterSetter( + 'FieldFormats' +); diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts index 1a186a6514763..cff48144489f0 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts @@ -21,19 +21,9 @@ import { CONTAINS_SPACES, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - isFilterable, validateIndexPattern, } from './utils'; -import { Field } from './fields'; - -const mockField = { - name: 'foo', - scripted: false, - searchable: true, - type: 'string', -} as Field; - describe('Index Pattern Utils', () => { describe('Validation', () => { it('should not allow space in the pattern', () => { @@ -52,42 +42,4 @@ describe('Index Pattern Utils', () => { expect(validateIndexPattern('my-pattern-*')).toEqual({}); }); }); - - describe('isFilterable', () => { - describe('types', () => { - it('should return true for filterable types', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { - expect(isFilterable({ ...mockField, type })).toBe(true); - }); - }); - - it('should return false for filterable types if the field is not searchable', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { - expect(isFilterable({ ...mockField, type, searchable: false })).toBe(false); - }); - }); - - it('should return false for un-filterable types', () => { - [ - 'geo_point', - 'geo_shape', - 'attachment', - 'murmur3', - '_source', - 'unknown', - 'conflict', - ].forEach(type => { - expect(isFilterable({ ...mockField, type })).toBe(false); - }); - }); - }); - - it('should return true for scripted fields', () => { - expect(isFilterable({ ...mockField, scripted: true, searchable: false })).toBe(true); - }); - - it('should return true for the _id field', () => { - expect(isFilterable({ ...mockField, name: '_id' })).toBe(true); - }); - }); }); diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 1c877f4f14251..8c2878a3ff9ba 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -19,9 +19,6 @@ import { find, get } from 'lodash'; -import { Field } from './fields'; -import { getFilterableKbnTypeNames } from '../../../../../plugins/data/public'; - import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; export const ILLEGAL_CHARACTERS = 'ILLEGAL_CHARACTERS'; @@ -74,19 +71,6 @@ export async function findIndexPatternByTitle( ); } -export async function getIndexPatternTitle( - client: SavedObjectsClientContract, - indexPatternId: string -): Promise> { - const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; - - if (savedObject.error) { - throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); - } - - return savedObject.attributes.title; -} - function indexPatternContainsSpaces(indexPattern: string): boolean { return indexPattern.includes(' '); } @@ -107,16 +91,6 @@ export function validateIndexPattern(indexPattern: string) { return errors; } -const filterableTypes = getFilterableKbnTypeNames(); - -export function isFilterable(field: Field): boolean { - return ( - field.name === '_id' || - field.scripted || - Boolean(field.searchable && filterableTypes.includes(field.type)) - ); -} - export function getFromSavedObject(savedObject: any) { if (get(savedObject, 'attributes.fields') === undefined) { return; diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 2059f61fde59e..da24576655d2b 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -94,6 +94,7 @@ export class DataPlugin implements Plugin void; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; + dataTestSubj?: string; disableAutoFocus?: boolean; screenTitle?: string; indexPatterns?: Array; @@ -189,6 +187,7 @@ function QueryBarTopRowUI(props: Props) { onChange={onQueryChange} onSubmit={onInputSubmit} persistedLog={persistedLog} + dataTestSubj={props.dataTestSubj} /> ); @@ -298,7 +297,7 @@ function QueryBarTopRowUI(props: Props) { language === 'kuery' && typeof query === 'string' && (!storage || !storage.get('kibana.luceneSyntaxWarningOptOut')) && - doesKueryExpressionHaveLuceneSyntaxError(query) + esKuery.doesKueryExpressionHaveLuceneSyntaxError(query) ) { const toast = notifications!.toasts.addWarning({ title: intl.formatMessage({ diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index db2a803ea1c61..7165de026920d 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -29,7 +29,12 @@ import { ExpressionFunction, KibanaDatatableColumn, } from 'src/plugins/expressions/public'; -import { SearchSource } from '../../../../../ui/public/courier/search_source'; +import { + SearchSource, + SearchSourceContract, + getRequestInspectorStats, + getResponseInspectorStats, +} from '../../../../../ui/public/courier'; // @ts-ignore import { FilterBarQueryFilterProvider, @@ -37,10 +42,6 @@ import { } from '../../../../../ui/public/filter_manager/query_filter'; import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier/utils/courier_inspector_utils'; import { calculateObjectHash } from '../../../../../ui/public/vis/lib/calculate_object_hash'; import { getTime } from '../../../../../ui/public/timefilter'; // @ts-ignore @@ -50,7 +51,7 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; export interface RequestHandlerParams { - searchSource: SearchSource; + searchSource: SearchSourceContract; aggs: AggConfigs; timeRange?: TimeRange; query?: Query; @@ -119,7 +120,7 @@ const handleCourierRequest = async ({ return aggs.toDsl(metricsAtAllLevels); }); - requestSearchSource.onRequestStart((paramSearchSource: SearchSource, options: any) => { + requestSearchSource.onRequestStart((paramSearchSource, options) => { return aggs.onSearchRequestStart(paramSearchSource, options); }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 44637365247fb..0ca9482fefa30 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../data/public', () => { +jest.mock('../../../../../../../plugins/data/public', () => { return { FilterBar: () =>
, + }; +}); + +jest.mock('../../../../../data/public', () => { + return { QueryBarInput: () =>
, }; }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index e97c06ace1579..6a1ef77a56653 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { IndexPattern, FilterBar } from '../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form'; @@ -35,12 +35,13 @@ import { withKibana, KibanaReactContextValue, } from '../../../../../../../plugins/kibana_react/public'; -import { IDataPluginServices } from '../../../types'; import { + IDataPluginServices, TimeRange, Query, esFilters, TimeHistoryContract, + FilterBar, } from '../../../../../../../plugins/data/public'; interface SearchBarInjectedDeps { @@ -64,7 +65,7 @@ export interface SearchBarOwnProps { isLoading?: boolean; customSubmitButton?: React.ReactNode; screenTitle?: string; - + dataTestSubj?: string; // Togglers showQueryBar?: boolean; showQueryInput?: boolean; @@ -415,6 +416,7 @@ class SearchBarUI extends Component { customSubmitButton={ this.props.customSubmitButton ? this.props.customSubmitButton : undefined } + dataTestSubj={this.props.dataTestSubj} /> ); } diff --git a/src/legacy/core_plugins/data/public/shim/legacy_module.ts b/src/legacy/core_plugins/data/public/shim/legacy_module.ts index edc389b411971..06c5caa04ba9a 100644 --- a/src/legacy/core_plugins/data/public/shim/legacy_module.ts +++ b/src/legacy/core_plugins/data/public/shim/legacy_module.ts @@ -19,58 +19,11 @@ import { once } from 'lodash'; -import { wrapInI18nContext } from 'ui/i18n'; - // @ts-ignore import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; -import { FilterBar } from '../filter'; import { IndexPatterns } from '../index_patterns/index_patterns'; /** @internal */ export const initLegacyModule = once((indexPatterns: IndexPatterns): void => { - uiModules - .get('app/kibana', ['react']) - .directive('filterBar', () => { - return { - restrict: 'E', - template: '', - compile: (elem: any) => { - const child = document.createElement('filter-bar-helper'); - - // Copy attributes to the child directive - for (const attr of elem[0].attributes) { - child.setAttribute(attr.name, attr.value); - } - - child.setAttribute('ui-settings', 'uiSettings'); - child.setAttribute('doc-links', 'docLinks'); - child.setAttribute('plugin-data-start', 'pluginDataStart'); - - // Append helper directive - elem.append(child); - - const linkFn = ($scope: any) => { - $scope.uiSettings = npStart.core.uiSettings; - $scope.docLinks = npStart.core.docLinks; - $scope.pluginDataStart = npStart.plugins.data; - }; - - return linkFn; - }, - }; - }) - .directive('filterBarHelper', (reactDirective: any) => { - return reactDirective(wrapInI18nContext(FilterBar), [ - ['uiSettings', { watchDepth: 'reference' }], - ['docLinks', { watchDepth: 'reference' }], - ['onFiltersUpdated', { watchDepth: 'reference' }], - ['indexPatterns', { watchDepth: 'collection' }], - ['filters', { watchDepth: 'collection' }], - ['className', { watchDepth: 'reference' }], - ['pluginDataStart', { watchDepth: 'reference' }], - ]); - }); - uiModules.get('kibana/index_patterns').value('indexPatterns', indexPatterns); }); diff --git a/src/legacy/core_plugins/data/public/types.ts b/src/legacy/core_plugins/data/public/types.ts deleted file mode 100644 index b6c9c47cc0ae6..0000000000000 --- a/src/legacy/core_plugins/data/public/types.ts +++ /dev/null @@ -1,32 +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 { UiSettingsClientContract, CoreStart } from 'src/core/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; - -export interface IDataPluginServices extends Partial { - appName: string; - uiSettings: UiSettingsClientContract; - savedObjects: CoreStart['savedObjects']; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - storage: IStorageWrapper; - data: DataPublicPluginStart; -} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js index 27f37421b0e25..45981adf9af45 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js @@ -17,8 +17,27 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('../../../../../core_plugins/data/public/legacy', () => ({ + indexPatterns: { + indexPatterns: { + get: jest.fn(), + } + } +})); + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js index 663a36ab69f46..c48123f3db714 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js @@ -20,12 +20,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { IndexPatternSelect } from 'ui/index_patterns'; - import { EuiFormRow, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function IndexPatternSelectFormRowUi(props) { const { controlIndex, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index ea029af9e4890..b37e8af0895fe 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -17,12 +17,24 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; + import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js index 5a698d65286ac..8d601f5a727d1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js @@ -17,19 +17,30 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); + import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; -import { - RangeControlEditor, -} from './range_control_editor'; +import { RangeControlEditor } from './range_control_editor'; const controlParams = { id: '1', diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js index 61a3d4084ab8f..2ab4131957c32 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js @@ -19,9 +19,9 @@ import { timefilter } from 'ui/timefilter'; export function createSearchSource(SearchSource, initialState, indexPattern, aggs, useTimeFilter, filters = []) { - const searchSource = new SearchSource(initialState); + const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals - searchSource.setParent(false); + searchSource.setParent(undefined); searchSource.setField('filter', () => { const activeFilters = [...filters]; if (useTimeFilter) { diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index c7cda8aec0165..91364071579ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -64,7 +64,6 @@ export default function (kibana) { hacks: [ 'plugins/kibana/dev_tools', ], - fieldFormats: ['plugins/kibana/field_formats/register'], savedObjectTypes: [ 'plugins/kibana/visualize/saved_visualizations/saved_visualization_register', 'plugins/kibana/discover/saved_searches/saved_search_register', @@ -325,6 +324,7 @@ export default function (kibana) { }, init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; // uuid await manageUuid(server); // routes @@ -339,8 +339,8 @@ export default function (kibana) { registerKqlTelemetryApi(server); registerFieldFormats(server); registerTutorials(server); - makeKQLUsageCollector(server); - registerCspCollector(server); + makeKQLUsageCollector(usageCollection, server); + registerCspCollector(usageCollection, server); server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap new file mode 100644 index 0000000000000..07e4173d5323f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -0,0 +1,516 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` + + + + + + + + + +

+ This dashboard is empty. Let’s fill it up! +

+

+ + Click the + + + + button in the menu bar above to add a visualization to the dashboard. + +

+

+ + visit the Visualize app + , + } + } + > + If you haven't set up any visualizations yet, + + visit the Visualize app + + to create your first visualization + +

+
+
+
+
+`; + +exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] = ` + + + + + + + + + +

+ This dashboard is empty. Let’s fill it up! +

+

+ + Click the + + + + button in the menu bar above to start working on your new dashboard. + +

+
+
+
+
+`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx new file mode 100644 index 0000000000000..a4604d17ddecd --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DashboardEmptyScreen, Props } from '../dashboard_empty_screen'; + +describe('DashboardEmptyScreen', () => { + const defaultProps = { + showLinkToVisualize: true, + onLinkClick: jest.fn(), + }; + + function mountComponent(props?: Props) { + const compProps = props || defaultProps; + const comp = mountWithIntl(); + return comp; + } + + test('renders correctly with visualize paragraph', () => { + const component = mountComponent(); + expect(component).toMatchSnapshot(); + const paragraph = component.find('.linkToVisualizeParagraph'); + expect(paragraph.length).toBe(1); + }); + + test('renders correctly without visualize paragraph', () => { + const component = mountComponent({ ...defaultProps, ...{ showLinkToVisualize: false } }); + expect(component).toMatchSnapshot(); + const paragraph = component.find('.linkToVisualizeParagraph'); + expect(paragraph.length).toBe(0); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts index 1f2094d68063d..d9dea35a8a1c0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { AppStateClass } from 'ui/state_management/app_state'; +import { AppStateClass } from '../legacy_imports'; /** * A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector. diff --git a/src/legacy/core_plugins/kibana/public/dashboard/_dashboard_app.scss b/src/legacy/core_plugins/kibana/public/dashboard/_dashboard_app.scss index eebfad5979d68..14c35759d70a9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/_dashboard_app.scss +++ b/src/legacy/core_plugins/kibana/public/dashboard/_dashboard_app.scss @@ -1,7 +1,7 @@ .dshAppContainer { - flex: 1; display: flex; flex-direction: column; + height: 100%; } .dshStartScreen { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts new file mode 100644 index 0000000000000..9c50adeeefccb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -0,0 +1,229 @@ +/* + * 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 { EuiConfirmModal, EuiIcon } from '@elastic/eui'; +import angular, { IModule } from 'angular'; +import { IPrivate } from 'ui/private'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { + AppMountContext, + ChromeStart, + LegacyCoreStart, + SavedObjectsClientContract, + UiSettingsClientContract, +} from 'kibana/public'; +import { Storage } from '../../../../../plugins/kibana_utils/public'; +import { + GlobalStateProvider, + StateManagementConfigProvider, + AppStateProvider, + PrivateProvider, + EventsProvider, + PersistedState, + createTopNavDirective, + createTopNavHelper, + PromiseServiceCreator, + KbnUrlProvider, + RedirectWhenMissingProvider, + confirmModalFactory, + configureAppAngularModule, +} from './legacy_imports'; + +// @ts-ignore +import { initDashboardApp } from './legacy_app'; +import { DataStart } from '../../../data/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { NavigationStart } from '../../../navigation/public'; +import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; +import { SharePluginStart } from '../../../../../plugins/share/public'; + +export interface RenderDeps { + core: LegacyCoreStart; + indexPatterns: DataStart['indexPatterns']['indexPatterns']; + dataStart: DataStart; + npDataStart: NpDataStart; + navigation: NavigationStart; + savedObjectsClient: SavedObjectsClientContract; + savedObjectRegistry: any; + dashboardConfig: any; + savedDashboards: any; + dashboardCapabilities: any; + uiSettings: UiSettingsClientContract; + chrome: ChromeStart; + addBasePath: (path: string) => string; + savedQueryService: DataStart['search']['services']['savedQueryService']; + embeddables: IEmbeddableStart; + localStorage: Storage; + share: SharePluginStart; +} + +let angularModuleInstance: IModule | null = null; + +export const renderApp = (element: HTMLElement, appBasePath: string, deps: RenderDeps) => { + if (!angularModuleInstance) { + angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); + // global routing stuff + configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true); + // custom routing stuff + initDashboardApp(angularModuleInstance, deps); + } + const $injector = mountDashboardApp(appBasePath, element); + return () => { + $injector.get('$rootScope').$destroy(); + }; +}; + +const mainTemplate = (basePath: string) => `
+ +
+
+`; + +const moduleName = 'app/dashboard'; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react']; + +function mountDashboardApp(appBasePath: string, element: HTMLElement) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%'); + // eslint-disable-next-line + mountpoint.innerHTML = mainTemplate(appBasePath); + // bootstrap angular into detached element and attach it later to + // make angular-within-angular possible + const $injector = angular.bootstrap(mountpoint, [moduleName]); + // initialize global state handler + element.appendChild(mountpoint); + return $injector; +} + +function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) { + createLocalI18nModule(); + createLocalPrivateModule(); + createLocalPromiseModule(); + createLocalConfigModule(core); + createLocalKbnUrlModule(); + createLocalStateModule(); + createLocalPersistedStateModule(); + createLocalTopNavModule(navigation); + createLocalConfirmModalModule(); + createLocalIconModule(); + + const dashboardAngularModule = angular.module(moduleName, [ + ...thirdPartyAngularDependencies, + 'app/dashboard/Config', + 'app/dashboard/I18n', + 'app/dashboard/Private', + 'app/dashboard/PersistedState', + 'app/dashboard/TopNav', + 'app/dashboard/State', + 'app/dashboard/ConfirmModal', + 'app/dashboard/icon', + 'app/dashboard/emptyScreen', + ]); + return dashboardAngularModule; +} + +function createLocalIconModule() { + angular + .module('app/dashboard/icon', ['react']) + .directive('icon', reactDirective => reactDirective(EuiIcon)); +} + +function createLocalConfirmModalModule() { + angular + .module('app/dashboard/ConfirmModal', ['react']) + .factory('confirmModal', confirmModalFactory) + .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); +} + +function createLocalStateModule() { + angular + .module('app/dashboard/State', [ + 'app/dashboard/Private', + 'app/dashboard/Config', + 'app/dashboard/KbnUrl', + 'app/dashboard/Promise', + 'app/dashboard/PersistedState', + ]) + .factory('AppState', function(Private: any) { + return Private(AppStateProvider); + }) + .service('getAppState', function(Private: any) { + return Private(AppStateProvider).getAppState; + }) + .service('globalState', function(Private: any) { + return Private(GlobalStateProvider); + }); +} + +function createLocalPersistedStateModule() { + angular + .module('app/dashboard/PersistedState', ['app/dashboard/Private', 'app/dashboard/Promise']) + .factory('PersistedState', (Private: IPrivate) => { + const Events = Private(EventsProvider); + return class AngularPersistedState extends PersistedState { + constructor(value: any, path: any) { + super(value, path, Events); + } + }; + }); +} + +function createLocalKbnUrlModule() { + angular + .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) + .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); +} + +function createLocalConfigModule(core: AppMountContext['core']) { + angular + .module('app/dashboard/Config', ['app/dashboard/Private']) + .provider('stateManagementConfig', StateManagementConfigProvider) + .provider('config', () => { + return { + $get: () => ({ + get: core.uiSettings.get.bind(core.uiSettings), + }), + }; + }); +} + +function createLocalPromiseModule() { + angular.module('app/dashboard/Promise', []).service('Promise', PromiseServiceCreator); +} + +function createLocalPrivateModule() { + angular.module('app/dashboard/Private', []).provider('Private', PrivateProvider); +} + +function createLocalTopNavModule(navigation: NavigationStart) { + angular + .module('app/dashboard/TopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); +} + +function createLocalI18nModule() { + angular + .module('app/dashboard/I18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index f644f3811e3e0..0b842fbfaeddc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -4,11 +4,11 @@ >
- -

-
-

-

- -

- - - -

-

+
-

-

- -

- - - -

+
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index d5da4ba51e55b..0ce8f2ef59fc0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -17,26 +17,16 @@ * under the License. */ -import _ from 'lodash'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { IInjector } from 'ui/chrome'; - -// @ts-ignore -import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; +import { StaticIndexPattern, SavedQuery } from 'plugins/data'; +import moment from 'moment'; +import { Subscription } from 'rxjs'; import { AppStateClass as TAppStateClass, AppState as TAppState, -} from 'ui/state_management/app_state'; - -import { KbnUrl } from 'ui/url/kbn_url'; -import { IndexPattern } from 'ui/index_patterns'; -import { IPrivate } from 'ui/private'; -import { StaticIndexPattern, SavedQuery } from 'plugins/data'; -import moment from 'moment'; -import { Subscription } from 'rxjs'; + IInjector, + KbnUrl, +} from './legacy_imports'; import { ViewMode } from '../../../embeddable_api/public/np_ready/public'; import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; @@ -44,6 +34,7 @@ import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types' import { TimeRange, Query, esFilters } from '../../../../../../src/plugins/data/public'; import { DashboardAppController } from './dashboard_app_controller'; +import { RenderDeps } from './application'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; @@ -90,54 +81,40 @@ export interface DashboardAppScope extends ng.IScope { kbnTopNav: any; enterEditMode: () => void; timefilterSubscriptions$: Subscription; + isVisible: boolean; } -const app = uiModules.get('app/dashboard', ['elasticsearch', 'ngRoute', 'react', 'kibana/config']); - -app.directive('dashboardApp', function($injector: IInjector) { - const AppState = $injector.get>('AppState'); - const kbnUrl = $injector.get('kbnUrl'); - const confirmModal = $injector.get('confirmModal'); - const config = $injector.get('config'); - - const Private = $injector.get('Private'); +export function initDashboardAppDirective(app: any, deps: RenderDeps) { + app.directive('dashboardApp', function($injector: IInjector) { + const AppState = $injector.get>('AppState'); + const kbnUrl = $injector.get('kbnUrl'); + const confirmModal = $injector.get('confirmModal'); + const config = deps.uiSettings; - const indexPatterns = $injector.get<{ - getDefault: () => Promise; - }>('indexPatterns'); - - return { - restrict: 'E', - controllerAs: 'dashboardApp', - controller: ( - $scope: DashboardAppScope, - $route: any, - $routeParams: { - id?: string; - }, - getAppState: { - previouslyStored: () => TAppState | undefined; - }, - dashboardConfig: { - getHideWriteControls: () => boolean; - }, - localStorage: { - get: (prop: string) => unknown; - } - ) => - new DashboardAppController({ - $route, - $scope, - $routeParams, - getAppState, - dashboardConfig, - localStorage, - Private, - kbnUrl, - AppStateClass: AppState, - indexPatterns, - config, - confirmModal, - }), - }; -}); + return { + restrict: 'E', + controllerAs: 'dashboardApp', + controller: ( + $scope: DashboardAppScope, + $route: any, + $routeParams: { + id?: string; + }, + getAppState: any, + globalState: any + ) => + new DashboardAppController({ + $route, + $scope, + $routeParams, + getAppState, + globalState, + kbnUrl, + AppStateClass: AppState, + config, + confirmModal, + ...deps, + }), + }; + }); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 457d8972876ae..1a0e13417d1e1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -23,41 +23,25 @@ import React from 'react'; import angular from 'angular'; import { uniq } from 'lodash'; -import chrome from 'ui/chrome'; -import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; -import { toastNotifications } from 'ui/notify'; - -// @ts-ignore -import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; - -import { docTitle } from 'ui/doc_title/doc_title'; - -import { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; - -import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; - -import { timefilter } from 'ui/timefilter'; - -import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider'; +import { Subscription } from 'rxjs'; import { + subscribeWithScope, + ConfirmationButtonTypes, + showSaveModal, + SaveResult, + migrateLegacyQuery, + State, AppStateClass as TAppStateClass, - AppState as TAppState, -} from 'ui/state_management/app_state'; - -import { KbnUrl } from 'ui/url/kbn_url'; -import { IndexPattern } from 'ui/index_patterns'; -import { IPrivate } from 'ui/private'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; -import { SaveOptions } from 'ui/saved_objects/saved_object'; -import { capabilities } from 'ui/capabilities'; -import { Subscription } from 'rxjs'; -import { npStart } from 'ui/new_platform'; -import { unhashUrl } from 'ui/state_management/state_hashing'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; + KbnUrl, + SaveOptions, + SavedObjectFinder, + unhashUrl, +} from './legacy_imports'; +import { FilterStateManager, IndexPattern, SavedQuery } from '../../../data/public'; import { Query } from '../../../../../plugins/data/public'; -import { start as data } from '../../../data/public/legacy'; + +import './dashboard_empty_screen_directive'; import { DashboardContainer, @@ -72,7 +56,6 @@ import { ViewMode, openAddPanelFlyout, } from '../../../embeddable_api/public/np_ready/public'; -import { start } from '../../../embeddable_api/public/np_ready/public/legacy'; import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types'; import { showOptionsPopover } from './top_nav/show_options_popover'; @@ -87,8 +70,23 @@ import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; - -const { savedQueryService } = data.search.services; +import { RenderDeps } from './application'; + +export interface DashboardAppControllerDependencies extends RenderDeps { + $scope: DashboardAppScope; + $route: any; + $routeParams: any; + getAppState: any; + globalState: State; + indexPatterns: { + getDefault: () => Promise; + }; + dashboardConfig: any; + kbnUrl: KbnUrl; + AppStateClass: TAppStateClass; + config: any; + confirmModal: ConfirmModalFn; +} export class DashboardAppController { // Part of the exposed plugin API - do not remove without careful consideration. @@ -101,58 +99,55 @@ export class DashboardAppController { $route, $routeParams, getAppState, + globalState, dashboardConfig, localStorage, - Private, kbnUrl, AppStateClass, indexPatterns, config, confirmModal, - }: { - $scope: DashboardAppScope; - $route: any; - $routeParams: any; - getAppState: { - previouslyStored: () => TAppState | undefined; - }; - indexPatterns: { - getDefault: () => Promise; - }; - dashboardConfig: any; - localStorage: { - get: (prop: string) => unknown; - }; - Private: IPrivate; - kbnUrl: KbnUrl; - AppStateClass: TAppStateClass; - config: any; - confirmModal: ConfirmModalFn; - }) { - const queryFilter = Private(FilterBarQueryFilterProvider); - const getUnhashableStates = Private(getUnhashableStatesProvider); + savedQueryService, + embeddables, + share, + dashboardCapabilities, + npDataStart: { + query: { + filterManager, + timefilter: { timefilter }, + }, + }, + core: { notifications, overlays, chrome, injectedMetadata }, + }: DashboardAppControllerDependencies) { + new FilterStateManager(globalState, getAppState, filterManager); + const queryFilter = filterManager; + + function getUnhashableStates(): State[] { + return [getAppState(), globalState].filter(Boolean); + } let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { - docTitle.change(dash.title); + chrome.docTitle.change(dash.title); } const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, AppStateClass, hideWriteControls: dashboardConfig.getHideWriteControls(), + kibanaVersion: injectedMetadata.getKibanaVersion(), }); $scope.appState = dashboardStateManager.getAppState(); - // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during + // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) { + if (dashboardStateManager.getIsTimeSavedWithDashboard() && !globalState.$inheritedGlobalState) { dashboardStateManager.syncTimefilterWithDashboard(timefilter); } - $scope.showSaveQuery = capabilities.get().dashboard.saveQuery as boolean; + $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const updateIndexPatterns = (container?: DashboardContainer) => { if (!container || isErrorEmbeddable(container)) { @@ -187,10 +182,7 @@ export class DashboardAppController { [key: string]: DashboardPanelState; } = {}; dashboardStateManager.getPanels().forEach((panel: SavedDashboardPanel) => { - embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState( - panel, - dashboardStateManager.getUseMargins() - ); + embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); }); let expandedPanelId; if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { @@ -239,7 +231,7 @@ export class DashboardAppController { let outputSubscription: Subscription | undefined; const dashboardDom = document.getElementById('dashboardViewport'); - const dashboardFactory = start.getEmbeddableFactory( + const dashboardFactory = embeddables.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE ) as DashboardContainerFactory; dashboardFactory @@ -334,7 +326,7 @@ export class DashboardAppController { // Push breadcrumbs to new header navigation const updateBreadcrumbs = () => { - chrome.breadcrumbs.set([ + chrome.setBreadcrumbs([ { text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { defaultMessage: 'Dashboard', @@ -495,7 +487,7 @@ export class DashboardAppController { }); $scope.$watch( - () => capabilities.get().dashboard.saveQuery, + () => dashboardCapabilities.saveQuery, newCapability => { $scope.showSaveQuery = newCapability as boolean; } @@ -595,7 +587,7 @@ export class DashboardAppController { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function(id) { if (id) { - toastNotifications.addSuccess({ + notifications.toasts.addSuccess({ title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', { defaultMessage: `Dashboard '{dashTitle}' was saved`, values: { dashTitle: dash.title }, @@ -606,14 +598,14 @@ export class DashboardAppController { if (dash.id !== $routeParams.id) { kbnUrl.change(createDashboardEditUrl(dash.id)); } else { - docTitle.change(dash.lastSavedTitle); + chrome.docTitle.change(dash.lastSavedTitle); updateViewMode(ViewMode.VIEW); } } return { id }; }) .catch(error => { - toastNotifications.addDanger({ + notifications.toasts.addDanger({ title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', { defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, values: { @@ -734,10 +726,10 @@ export class DashboardAppController { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { openAddPanelFlyout({ embeddable: dashboardContainer, - getAllFactories: start.getEmbeddableFactories, - getFactory: start.getEmbeddableFactory, - notifications: npStart.core.notifications, - overlays: npStart.core.overlays, + getAllFactories: embeddables.getEmbeddableFactories, + getFactory: embeddables.getEmbeddableFactory, + notifications, + overlays, SavedObjectFinder, }); } @@ -757,7 +749,7 @@ export class DashboardAppController { }); }; navActions[TopNavIds.SHARE] = anchorElement => { - npStart.plugins.share.toggleShareContextMenu({ + share.toggleShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: !dashboardConfig.getHideWriteControls(), @@ -784,8 +776,15 @@ export class DashboardAppController { }, }); + const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => { + $scope.$evalAsync(() => { + $scope.isVisible = isVisible; + }); + }); + $scope.$on('$destroy', () => { updateSubscription.unsubscribe(); + visibleSubscription.unsubscribe(); $scope.timefilterSubscriptions$.unsubscribe(); dashboardStateManager.destroy(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx new file mode 100644 index 0000000000000..d5a4e6e6a325d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; +import { EuiIcon, EuiLink } from '@elastic/eui'; +import * as constants from './dashboard_empty_screen_constants'; + +export interface Props { + showLinkToVisualize: boolean; + onLinkClick: () => void; +} + +export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick }: Props) { + const linkToVisualizeParagraph = ( +

+ + {constants.visualizeAppLinkTest} + + ), + }} + /> +

+ ); + const paragraph = ( + description1: string, + description2: string, + linkText: string, + ariaLabel: string, + dataTestSubj?: string + ) => { + return ( +

+ + {description1} + + {linkText} + + {description2} + +

+ ); + }; + const addVisualizationParagraph = ( + + {paragraph( + constants.addVisualizationDescription1, + constants.addVisualizationDescription2, + constants.addVisualizationLinkText, + constants.addVisualizationLinkAriaLabel, + 'emptyDashboardAddPanelButton' + )} + {linkToVisualizeParagraph} + + ); + const enterEditModeParagraph = paragraph( + constants.howToStartWorkingOnNewDashboardDescription1, + constants.howToStartWorkingOnNewDashboardDescription2, + constants.howToStartWorkingOnNewDashboardEditLinkText, + constants.howToStartWorkingOnNewDashboardEditLinkAriaLabel + ); + return ( + + + +

{constants.fillDashboardTitle}

+ {showLinkToVisualize ? addVisualizationParagraph : enterEditModeParagraph} +
+
+ ); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx new file mode 100644 index 0000000000000..0f510375aaf59 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx @@ -0,0 +1,78 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const addVisualizationDescription1: string = i18n.translate( + 'kbn.dashboard.addVisualizationDescription1', + { + defaultMessage: 'Click the ', + } +); +export const addVisualizationDescription2: string = i18n.translate( + 'kbn.dashboard.addVisualizationDescription2', + { + defaultMessage: ' button in the menu bar above to add a visualization to the dashboard.', + } +); +export const addVisualizationLinkText: string = i18n.translate( + 'kbn.dashboard.addVisualizationLinkText', + { + defaultMessage: 'Add', + } +); +export const addVisualizationLinkAriaLabel: string = i18n.translate( + 'kbn.dashboard.addVisualizationLinkAriaLabel', + { + defaultMessage: 'Add visualization', + } +); +export const howToStartWorkingOnNewDashboardDescription1: string = i18n.translate( + 'kbn.dashboard.howToStartWorkingOnNewDashboardDescription1', + { + defaultMessage: 'Click the ', + } +); +export const howToStartWorkingOnNewDashboardDescription2: string = i18n.translate( + 'kbn.dashboard.howToStartWorkingOnNewDashboardDescription2', + { + defaultMessage: ' button in the menu bar above to start working on your new dashboard.', + } +); +export const howToStartWorkingOnNewDashboardEditLinkText: string = i18n.translate( + 'kbn.dashboard.howToStartWorkingOnNewDashboardEditLinkText', + { + defaultMessage: 'Edit', + } +); +export const howToStartWorkingOnNewDashboardEditLinkAriaLabel: string = i18n.translate( + 'kbn.dashboard.howToStartWorkingOnNewDashboardEditLinkAriaLabel', + { + defaultMessage: 'Edit dashboard', + } +); +export const fillDashboardTitle: string = i18n.translate('kbn.dashboard.fillDashboardTitle', { + defaultMessage: 'This dashboard is empty. Let\u2019s fill it up!', +}); +export const visualizeAppLinkTest: string = i18n.translate( + 'kbn.dashboard.visitVisualizeAppLinkText', + { + defaultMessage: 'visit the Visualize app', + } +); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_directive.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_directive.ts new file mode 100644 index 0000000000000..5ebefd817ca4a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_directive.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// @ts-ignore +import angular from 'angular'; +import { DashboardEmptyScreen } from './dashboard_empty_screen'; + +angular + .module('app/dashboard/emptyScreen', ['react']) + .directive('dashboardEmptyScreen', function(reactDirective: any) { + return reactDirective(DashboardEmptyScreen, [ + ['showLinkToVisualize', { watchDepth: 'value' }], + ['onLinkClick', { watchDepth: 'reference' }], + ]); + }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts index 5e81373001bf5..c236ac7843c03 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -21,16 +21,13 @@ import './np_core.test.mocks'; import { DashboardStateManager } from './dashboard_state_manager'; import { getAppStateMock, getSavedDashboardMock } from './__tests__'; -import { AppStateClass } from 'ui/state_management/app_state'; +import { AppStateClass } from './legacy_imports'; import { DashboardAppState } from './types'; -import { TimeRange, TimefilterContract } from 'src/plugins/data/public'; +import { TimeRange, TimefilterContract, InputTimeRange } from 'src/plugins/data/public'; import { ViewMode } from 'src/plugins/embeddable/public'; -import { InputTimeRange } from 'ui/timefilter'; -jest.mock('ui/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn(), - }, +jest.mock('ui/state_management/state', () => ({ + State: {}, })); describe('DashboardState', function() { @@ -52,6 +49,7 @@ describe('DashboardState', function() { savedDashboard, AppStateClass: getAppStateMock() as AppStateClass, hideWriteControls: false, + kibanaVersion: '7.0.0', }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts index d5af4c93d0e0c..ac8628ec2a9d9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -20,15 +20,21 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; -import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; -import { Timefilter } from 'ui/timefilter'; -import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state'; -import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; import { Moment } from 'moment'; import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { Query, esFilters } from '../../../../../../src/plugins/data/public'; +import { + stateMonitorFactory, + StateMonitor, + AppStateClass as TAppStateClass, + migrateLegacyQuery, +} from './legacy_imports'; +import { + Query, + esFilters, + TimefilterContract as Timefilter, +} from '../../../../../../src/plugins/data/public'; import { getAppStateDefaults, migrateAppState } from './lib'; import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; @@ -54,6 +60,7 @@ export class DashboardStateManager { }; private stateDefaults: DashboardAppStateDefaults; private hideWriteControls: boolean; + private kibanaVersion: string; public isDirty: boolean; private changeListeners: Array<(status: { dirty: boolean }) => void>; private stateMonitor: StateMonitor; @@ -68,11 +75,14 @@ export class DashboardStateManager { savedDashboard, AppStateClass, hideWriteControls, + kibanaVersion, }: { savedDashboard: SavedObjectDashboard; AppStateClass: TAppStateClass; hideWriteControls: boolean; + kibanaVersion: string; }) { + this.kibanaVersion = kibanaVersion; this.savedDashboard = savedDashboard; this.hideWriteControls = hideWriteControls; @@ -84,7 +94,7 @@ export class DashboardStateManager { // appState based on the URL (the url trumps the defaults). This means if we update the state format at all and // want to handle BWC, we must not only migrate the data stored with saved Dashboard, but also any old state in the // url. - migrateAppState(this.appState); + migrateAppState(this.appState, kibanaVersion); this.isDirty = false; @@ -146,7 +156,8 @@ export class DashboardStateManager { } convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel( - panelState + panelState, + this.kibanaVersion ); if ( diff --git a/src/legacy/core_plugins/kibana/public/dashboard/global_state_sync.ts b/src/legacy/core_plugins/kibana/public/dashboard/global_state_sync.ts new file mode 100644 index 0000000000000..8a733f940734b --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/global_state_sync.ts @@ -0,0 +1,67 @@ +/* + * 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 { State } from './legacy_imports'; +import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; + +/** + * Helper function to sync the global state with the various state providers + * when a local angular application mounts. There are three different ways + * global state can be passed into the application: + * * parameter in the URL hash - e.g. shared link + * * in-memory state in the data plugin exports (timefilter and filterManager) - e.g. default values + * + * This function looks up the three sources (earlier in the list means it takes precedence), + * puts it into the globalState object and syncs it with the url. + * + * Currently the legacy chrome takes care of restoring the global state when navigating from + * one app to another - to migrate away from that it will become necessary to also write the current + * state to local storage + */ +export function syncOnMount( + globalState: State, + { + query: { + filterManager, + timefilter: { timefilter }, + }, + }: NpDataStart +) { + // pull in global state information from the URL + globalState.fetch(); + // remember whether there were info in the URL + const hasGlobalURLState = Boolean(Object.keys(globalState.toObject()).length); + + // sync kibana platform state with the angular global state + if (!globalState.time) { + globalState.time = timefilter.getTime(); + } + if (!globalState.refreshInterval) { + globalState.refreshInterval = timefilter.getRefreshInterval(); + } + if (!globalState.filters && filterManager.getGlobalFilters().length > 0) { + globalState.filters = filterManager.getGlobalFilters(); + } + // only inject cross app global state if there is none in the url itself (that takes precedence) + if (hasGlobalURLState) { + // set flag the global state is set from the URL + globalState.$inheritedGlobalState = true; + } + globalState.save(); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu.js b/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu.js deleted file mode 100644 index 56b2bd253381c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu.js +++ /dev/null @@ -1,42 +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, { Fragment, PureComponent } from 'react'; -import { EuiButton, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; - -export class HelpMenu extends PureComponent { - render() { - return ( - - - - - - - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu_util.js b/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu_util.js index aeabff2d97007..55abfa179b56d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu_util.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/help_menu/help_menu_util.js @@ -17,15 +17,19 @@ * under the License. */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { HelpMenu } from './help_menu'; +import { i18n } from '@kbn/i18n'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; export function addHelpMenuToAppChrome(chrome) { - chrome.helpExtension.set(domElement => { - render(, domElement); - return () => { - unmountComponentAtNode(domElement); - }; + chrome.setHelpExtension({ + appName: i18n.translate('kbn.dashboard.helpMenu.appName', { + defaultMessage: 'Dashboards', + }), + links: [ + { + linkType: 'documentation', + href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, + }, + ], }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js deleted file mode 100644 index 712e05c92e5e8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ /dev/null @@ -1,207 +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 './dashboard_app'; -import { i18n } from '@kbn/i18n'; -import './saved_dashboard/saved_dashboards'; -import './dashboard_config'; -import uiRoutes from 'ui/routes'; -import chrome from 'ui/chrome'; -import { wrapInI18nContext } from 'ui/i18n'; -import { toastNotifications } from 'ui/notify'; - -import dashboardTemplate from './dashboard_app.html'; -import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html'; - -import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -import { InvalidJSONProperty, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; -import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; -import { uiModules } from 'ui/modules'; -import 'ui/capabilities/route_setup'; -import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; - -import { npStart } from 'ui/new_platform'; - -// load directives -import '../../../data/public'; - -const app = uiModules.get('app/dashboard', [ - 'ngRoute', - 'react', -]); - -app.directive('dashboardListing', function (reactDirective) { - return reactDirective(wrapInI18nContext(DashboardListing)); -}); - -function createNewDashboardCtrl($scope) { - $scope.visitVisualizeAppLinkText = i18n.translate('kbn.dashboard.visitVisualizeAppLinkText', { - defaultMessage: 'visit the Visualize app', - }); - addHelpMenuToAppChrome(chrome); -} - -uiRoutes - .defaults(/dashboard/, { - requireDefaultIndex: true, - requireUICapability: 'dashboard.show', - badge: uiCapabilities => { - if (uiCapabilities.dashboard.showWriteControls) { - return undefined; - } - - return { - text: i18n.translate('kbn.dashboard.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('kbn.dashboard.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save dashboards', - }), - iconType: 'glasses' - }; - } - }) - .when(DashboardConstants.LANDING_PAGE_PATH, { - template: dashboardListingTemplate, - controller($injector, $location, $scope, Private, config) { - const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; - const kbnUrl = $injector.get('kbnUrl'); - const dashboardConfig = $injector.get('dashboardConfig'); - - $scope.listingLimit = config.get('savedObjects:listingLimit'); - $scope.create = () => { - kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL); - }; - $scope.find = (search) => { - return services.dashboards.find(search, $scope.listingLimit); - }; - $scope.editItem = ({ id }) => { - kbnUrl.redirect(`${createDashboardEditUrl(id)}?_a=(viewMode:edit)`); - }; - $scope.getViewUrl = ({ id }) => { - return chrome.addBasePath(`#${createDashboardEditUrl(id)}`); - }; - $scope.delete = (dashboards) => { - return services.dashboards.delete(dashboards.map(d => d.id)); - }; - $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); - $scope.initialFilter = ($location.search()).filter || EMPTY_FILTER; - chrome.breadcrumbs.set([{ - text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', { - defaultMessage: 'Dashboards', - }), - }]); - addHelpMenuToAppChrome(chrome); - }, - resolve: { - dash: function ($route, Private, redirectWhenMissing, kbnUrl) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - const title = $route.current.params.title; - if (title) { - return savedObjectsClient.find({ - search: `"${title}"`, - search_fields: 'title', - type: 'dashboard', - }).then(results => { - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = results.savedObjects.filter( - dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase()); - if (matchingDashboards.length === 1) { - kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id)); - } else { - kbnUrl.redirect(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); - } - throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN; - }).catch(redirectWhenMissing({ - 'dashboard': DashboardConstants.LANDING_PAGE_PATH - })); - } - } - } - }) - .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { - template: dashboardTemplate, - controller: createNewDashboardCtrl, - requireUICapability: 'dashboard.createNew', - resolve: { - dash: function (savedDashboards, redirectWhenMissing) { - return savedDashboards.get() - .catch(redirectWhenMissing({ - 'dashboard': DashboardConstants.LANDING_PAGE_PATH - })); - } - } - }) - .when(createDashboardEditUrl(':id'), { - template: dashboardTemplate, - controller: createNewDashboardCtrl, - resolve: { - dash: function (savedDashboards, $route, redirectWhenMissing, kbnUrl, AppState) { - const id = $route.current.params.id; - - return savedDashboards.get(id) - .then((savedDashboard) => { - npStart.core.chrome.recentlyAccessed.add(savedDashboard.getFullPath(), savedDashboard.title, id); - return savedDashboard; - }) - .catch((error) => { - // A corrupt dashboard was detected (e.g. with invalid JSON properties) - if (error instanceof InvalidJSONProperty) { - toastNotifications.addDanger(error.message); - kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH); - return; - } - - // Preserve BWC of v5.3.0 links for new, unsaved dashboards. - // See https://github.com/elastic/kibana/issues/10951 for more context. - if (error instanceof SavedObjectNotFound && id === 'create') { - // Note "new AppState" is necessary so the state in the url is preserved through the redirect. - kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); - toastNotifications.addWarning(i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', - { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.' } - )); - } else { - throw error; - } - }) - .catch(redirectWhenMissing({ - 'dashboard': DashboardConstants.LANDING_PAGE_PATH - })); - } - } - }); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'dashboard', - title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { - defaultMessage: 'Dashboard', - }), - description: i18n.translate('kbn.dashboard.featureCatalogue.dashboardDescription', { - defaultMessage: 'Display and share a collection of visualizations and saved searches.', - }), - icon: 'dashboardApp', - path: `/app/kibana#${DashboardConstants.LANDING_PAGE_PATH}`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts new file mode 100644 index 0000000000000..d37cf5d7139ec --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -0,0 +1,67 @@ +/* + * 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 { + npSetup, + npStart, + SavedObjectRegistryProvider, + legacyChrome, + IPrivate, +} from './legacy_imports'; +import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin'; +import { start as data } from '../../../data/public/legacy'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import { start as navigation } from '../../../navigation/public/legacy'; +import './saved_dashboard/saved_dashboards'; +import './dashboard_config'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + const savedObjectRegistry = Private(SavedObjectRegistryProvider); + + return { + dashboardConfig: injector.get('dashboardConfig'), + savedObjectRegistry, + savedDashboards: injector.get('savedDashboards'), + }; +} + +(async () => { + const instance = new DashboardPlugin(); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + data, + npData: npStart.plugins.data, + embeddables, + navigation, + }); +})(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/legacy_app.js new file mode 100644 index 0000000000000..c7f2adb4b875b --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_app.js @@ -0,0 +1,224 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import dashboardTemplate from './dashboard_app.html'; +import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html'; + +import { ensureDefaultIndexPattern } from './legacy_imports'; +import { initDashboardAppDirective } from './dashboard_app'; +import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; +import { + InvalidJSONProperty, + SavedObjectNotFound, +} from '../../../../../plugins/kibana_utils/public'; +import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; +import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; +import { registerTimefilterWithGlobalStateFactory } from '../../../../ui/public/timefilter/setup_router'; +import { syncOnMount } from './global_state_sync'; + +export function initDashboardApp(app, deps) { + initDashboardAppDirective(app, deps); + + app.directive('dashboardListing', function (reactDirective) { + return reactDirective(DashboardListing); + }); + + function createNewDashboardCtrl($scope) { + $scope.visitVisualizeAppLinkText = i18n.translate('kbn.dashboard.visitVisualizeAppLinkText', { + defaultMessage: 'visit the Visualize app', + }); + addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks); + } + + app.run(globalState => { + syncOnMount(globalState, deps.npDataStart); + }); + + app.run((globalState, $rootScope) => { + registerTimefilterWithGlobalStateFactory( + deps.npDataStart.query.timefilter.timefilter, + globalState, + $rootScope + ); + }); + + app.config(function ($routeProvider) { + const defaults = { + reloadOnSearch: false, + requireUICapability: 'dashboard.show', + badge: () => { + if (deps.dashboardCapabilities.showWriteControls) { + return undefined; + } + + return { + text: i18n.translate('kbn.dashboard.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('kbn.dashboard.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save dashboards', + }), + iconType: 'glasses', + }; + }, + }; + + $routeProvider + .when(DashboardConstants.LANDING_PAGE_PATH, { + ...defaults, + template: dashboardListingTemplate, + controller($injector, $location, $scope) { + const services = deps.savedObjectRegistry.byLoaderPropertiesName; + const kbnUrl = $injector.get('kbnUrl'); + const dashboardConfig = deps.dashboardConfig; + + $scope.listingLimit = deps.uiSettings.get('savedObjects:listingLimit'); + $scope.create = () => { + kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL); + }; + $scope.find = search => { + return services.dashboards.find(search, $scope.listingLimit); + }; + $scope.editItem = ({ id }) => { + kbnUrl.redirect(`${createDashboardEditUrl(id)}?_a=(viewMode:edit)`); + }; + $scope.getViewUrl = ({ id }) => { + return deps.addBasePath(`#${createDashboardEditUrl(id)}`); + }; + $scope.delete = dashboards => { + return services.dashboards.delete(dashboards.map(d => d.id)); + }; + $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); + $scope.initialFilter = $location.search().filter || EMPTY_FILTER; + deps.chrome.setBreadcrumbs([ + { + text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', { + defaultMessage: 'Dashboards', + }), + }, + ]); + addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks); + }, + resolve: { + dash: function ($rootScope, $route, redirectWhenMissing, kbnUrl) { + return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl).then(() => { + const savedObjectsClient = deps.savedObjectsClient; + const title = $route.current.params.title; + if (title) { + return savedObjectsClient + .find({ + search: `"${title}"`, + search_fields: 'title', + type: 'dashboard', + }) + .then(results => { + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id)); + } else { + kbnUrl.redirect(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); + } + $rootScope.$digest(); + return new Promise(() => {}); + }); + } + }); + }, + }, + }) + .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { + ...defaults, + template: dashboardTemplate, + controller: createNewDashboardCtrl, + requireUICapability: 'dashboard.createNew', + resolve: { + dash: function (redirectWhenMissing, $rootScope, kbnUrl) { + return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl) + .then(() => { + return deps.savedDashboards.get(); + }) + .catch( + redirectWhenMissing({ + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }) + ); + }, + }, + }) + .when(createDashboardEditUrl(':id'), { + ...defaults, + template: dashboardTemplate, + controller: createNewDashboardCtrl, + resolve: { + dash: function ($rootScope, $route, redirectWhenMissing, kbnUrl, AppState) { + const id = $route.current.params.id; + + return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl) + .then(() => { + return deps.savedDashboards.get(id); + }) + .then(savedDashboard => { + deps.chrome.recentlyAccessed.add( + savedDashboard.getFullPath(), + savedDashboard.title, + id + ); + return savedDashboard; + }) + .catch(error => { + // A corrupt dashboard was detected (e.g. with invalid JSON properties) + if (error instanceof InvalidJSONProperty) { + deps.toastNotifications.addDanger(error.message); + kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH); + return; + } + + // Preserve BWC of v5.3.0 links for new, unsaved dashboards. + // See https://github.com/elastic/kibana/issues/10951 for more context. + if (error instanceof SavedObjectNotFound && id === 'create') { + // Note "new AppState" is necessary so the state in the url is preserved through the redirect. + kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); + deps.toastNotifications.addWarning( + i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: + 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }) + ); + return new Promise(() => {}); + } else { + throw error; + } + }) + .catch( + redirectWhenMissing({ + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }) + ); + }, + }, + }) + .when(`dashboard/:tail*?`, { redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}` }) + .when(`dashboards/:tail*?`, { redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}` }); + }); +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts new file mode 100644 index 0000000000000..7c3c389330887 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/** + * The imports in this file are static functions and types which still live in legacy folders and are used + * within dashboard. To consolidate them all in one place, they are re-exported from this file. Eventually + * this list should become empty. Imports from the top level of shimmed or moved plugins can be imported + * directly where they are needed. + */ + +import chrome from 'ui/chrome'; + +export const legacyChrome = chrome; +export { State } from 'ui/state_management/state'; +export { AppState } from 'ui/state_management/app_state'; +export { AppStateClass } from 'ui/state_management/app_state'; +export { SaveOptions } from 'ui/saved_objects/saved_object'; +export { npSetup, npStart } from 'ui/new_platform'; +export { SavedObjectRegistryProvider } from 'ui/saved_objects'; +export { IPrivate } from 'ui/private'; +export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; +export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; +// @ts-ignore +export { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; +export { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; +export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +export { KbnUrl } from 'ui/url/kbn_url'; +// @ts-ignore +export { GlobalStateProvider } from 'ui/state_management/global_state'; +// @ts-ignore +export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; +// @ts-ignore +export { AppStateProvider } from 'ui/state_management/app_state'; +// @ts-ignore +export { PrivateProvider } from 'ui/private/private'; +// @ts-ignore +export { EventsProvider } from 'ui/events'; +export { PersistedState } from 'ui/persisted_state'; +// @ts-ignore +export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; +// @ts-ignore +export { PromiseServiceCreator } from 'ui/promises/promises'; +// @ts-ignore +export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +// @ts-ignore +export { confirmModalFactory } from 'ui/modals/confirm_modal'; +export { configureAppAngularModule } from 'ui/legacy_compat'; +export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; +export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; +export { unhashUrl } from 'ui/state_management/state_hashing'; +export { IInjector } from 'ui/chrome'; +export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts index 99bb6b115b985..3f04cad4f322b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts @@ -48,7 +48,7 @@ test('convertSavedDashboardPanelToPanelState', () => { version: '7.0.0', }; - expect(convertSavedDashboardPanelToPanelState(savedDashboardPanel, true)).toEqual({ + expect(convertSavedDashboardPanelToPanelState(savedDashboardPanel)).toEqual({ gridData: { x: 0, y: 0, @@ -82,7 +82,7 @@ test('convertSavedDashboardPanelToPanelState does not include undefined id', () version: '7.0.0', }; - const converted = convertSavedDashboardPanelToPanelState(savedDashboardPanel, false); + const converted = convertSavedDashboardPanelToPanelState(savedDashboardPanel); expect(converted.hasOwnProperty('savedObjectId')).toBe(false); }); @@ -103,7 +103,7 @@ test('convertPanelStateToSavedDashboardPanel', () => { type: 'search', }; - expect(convertPanelStateToSavedDashboardPanel(dashboardPanel)).toEqual({ + expect(convertPanelStateToSavedDashboardPanel(dashboardPanel, '6.3.0')).toEqual({ type: 'search', embeddableConfig: { something: 'hi!', @@ -137,6 +137,6 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n type: 'search', }; - const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel); + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts index 4a3bc3b228106..2d42609e1e25f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts @@ -18,12 +18,10 @@ */ import { omit } from 'lodash'; import { DashboardPanelState } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; -import chrome from 'ui/chrome'; import { SavedDashboardPanel } from '../types'; export function convertSavedDashboardPanelToPanelState( - savedDashboardPanel: SavedDashboardPanel, - useMargins: boolean + savedDashboardPanel: SavedDashboardPanel ): DashboardPanelState { return { type: savedDashboardPanel.type, @@ -38,13 +36,14 @@ export function convertSavedDashboardPanelToPanelState( } export function convertPanelStateToSavedDashboardPanel( - panelState: DashboardPanelState + panelState: DashboardPanelState, + version: string ): SavedDashboardPanel { const customTitle: string | undefined = panelState.explicitInput.title ? (panelState.explicitInput.title as string) : undefined; return { - version: chrome.getKibanaVersion(), + version, type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts index 10c27226300a5..4aa2461bb6593 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts @@ -43,7 +43,7 @@ test('migrate app state from 6.0', async () => { getQueryParamName: () => 'a', save: mockSave, }; - migrateAppState(appState); + migrateAppState(appState, '8.0'); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -58,6 +58,7 @@ test('migrate app state from 6.0', async () => { }); test('migrate sort from 6.1', async () => { + const TARGET_VERSION = '8.0'; const mockSave = jest.fn(); const appState = { uiState: { @@ -80,7 +81,7 @@ test('migrate sort from 6.1', async () => { save: mockSave, useMargins: false, }; - migrateAppState(appState); + migrateAppState(appState, TARGET_VERSION); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -112,7 +113,7 @@ test('migrates 6.0 even when uiState does not exist', async () => { getQueryParamName: () => 'a', save: mockSave, }; - migrateAppState(appState); + migrateAppState(appState, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -147,7 +148,7 @@ test('6.2 migration adjusts w & h without margins', async () => { save: mockSave, useMargins: false, }; - migrateAppState(appState); + migrateAppState(appState, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -184,7 +185,7 @@ test('6.2 migration adjusts w & h with margins', async () => { save: mockSave, useMargins: true, }; - migrateAppState(appState); + migrateAppState(appState, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts index 9bd93029f06d8..c4ad754548459 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts @@ -18,7 +18,6 @@ */ import semver from 'semver'; -import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public'; import { @@ -37,7 +36,10 @@ import { migratePanelsTo730 } from '../migrations/migrate_to_730_panels'; * * Once we hit a major version, we can remove support for older style URLs and get rid of this logic. */ -export function migrateAppState(appState: { [key: string]: unknown } | DashboardAppState) { +export function migrateAppState( + appState: { [key: string]: unknown } | DashboardAppState, + kibanaVersion: string +) { if (!appState.panels) { throw new Error( i18n.translate('kbn.dashboard.panel.invalidData', { @@ -73,7 +75,7 @@ export function migrateAppState(appState: { [key: string]: unknown } | Dashboard | SavedDashboardPanel630 | SavedDashboardPanel640To720 >, - chrome.getKibanaVersion(), + kibanaVersion, appState.useMargins, appState.uiState ); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts index 168f320b5ea7e..e0d82373d3ad9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SaveOptions } from 'ui/saved_objects/saved_object'; -import { Timefilter } from 'ui/timefilter'; +import { TimefilterContract } from 'src/plugins/data/public'; +import { SaveOptions } from '../legacy_imports'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; @@ -32,7 +32,7 @@ import { DashboardStateManager } from '../dashboard_state_manager'; */ export function saveDashboard( toJson: (obj: any) => string, - timeFilter: Timefilter, + timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, saveOptions: SaveOptions ): Promise { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts index 707b5a0f5f5f5..ce9096b3a56f0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts @@ -18,16 +18,15 @@ */ import _ from 'lodash'; -import { AppState } from 'ui/state_management/app_state'; -import { Timefilter } from 'ui/timefilter'; -import { RefreshInterval } from 'src/plugins/data/public'; +import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public'; +import { AppState } from '../legacy_imports'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; export function updateSavedDashboard( savedDashboard: SavedObjectDashboard, appState: AppState, - timeFilter: Timefilter, + timeFilter: TimefilterContract, toJson: (object: T) => string ) { savedDashboard.title = appState.title; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 1ed05035f5f4c..b2f004568841a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -1,533 +1,545 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`after fetch hideWriteControls 1`] = ` - - - - - } - /> -
- } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, + + + + + + } + /> +
+ } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; exports[`after fetch initialFilter 1`] = ` - - - - - } - body={ - -

+ + + -

-

- - - , + + } + body={ + +

+ +

+

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

+
+ } + iconType="dashboardApp" + title={ +

+ -

- - } - iconType="dashboardApp" - title={ -

- -

- } - /> -

- } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, + + } + /> +
+ } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; exports[`after fetch renders call to action when no dashboards exist 1`] = ` - - - - - } - body={ - -

+ + + -

-

- - - , + + } + body={ + +

+ +

+

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

+
+ } + iconType="dashboardApp" + title={ +

+ -

- - } - iconType="dashboardApp" - title={ -

- -

- } - /> -

- } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, + + } + /> +
+ } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; exports[`after fetch renders table rows 1`] = ` - - - - - } - body={ - -

+ + + -

-

- - - , + + } + body={ + +

+ +

+

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

+
+ } + iconType="dashboardApp" + title={ +

+ -

- - } - iconType="dashboardApp" - title={ -

- -

- } - /> -

- } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, + + } + /> +
+ } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` - - - - - } - body={ - -

+ + + -

-

- - - , + + } + body={ + +

+ +

+

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

+
+ } + iconType="dashboardApp" + title={ +

+ -

- - } - iconType="dashboardApp" - title={ -

- -

- } - /> -

- } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, + + } + /> +
+ } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; exports[`renders empty page in before initial fetch to avoid flickering 1`] = ` - - - - - } - body={ - -

+ + + -

-

- - - , + + } + body={ + +

+ +

+

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

+
+ } + iconType="dashboardApp" + title={ +

+ -

- - } - iconType="dashboardApp" - title={ -

- -

- } - /> - - } - tableColumns={ - Array [ - Object { - "field": "title", - "name": "Title", - "render": [Function], - "sortable": true, - }, +

+ } + /> + + } + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "dataType": "string", + "field": "description", + "name": "Description", + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={Object {}} + uiSettings={ Object { - "dataType": "string", - "field": "description", - "name": "Description", - "sortable": true, - }, - ] - } - tableListTitle="Dashboards" - toastNotifications={Object {}} - uiSettings={ - Object { - "get": [MockFunction], + "get": [MockFunction], + } } - } -/> + /> + `; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index c222fcd3c928c..98581223afa46 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -19,7 +19,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; @@ -41,27 +41,29 @@ export class DashboardListing extends React.Component { render() { return ( - + + + ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts new file mode 100644 index 0000000000000..609bd717f3c48 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -0,0 +1,147 @@ +/* + * 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 { + App, + CoreSetup, + CoreStart, + LegacyCoreStart, + Plugin, + SavedObjectsClientContract, +} from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { RenderDeps } from './application'; +import { DataStart } from '../../../data/public'; +import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { Storage } from '../../../../../plugins/kibana_utils/public'; +import { NavigationStart } from '../../../navigation/public'; +import { DashboardConstants } from './dashboard_constants'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../../plugins/home/public'; +import { SharePluginStart } from '../../../../../plugins/share/public'; +import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; + +export interface LegacyAngularInjectedDependencies { + dashboardConfig: any; + savedObjectRegistry: any; + savedDashboards: any; +} + +export interface DashboardPluginStartDependencies { + data: DataStart; + npData: NpDataStart; + embeddables: IEmbeddableStart; + navigation: NavigationStart; + share: SharePluginStart; +} + +export interface DashboardPluginSetupDependencies { + __LEGACY: { + getAngularDependencies: () => Promise; + }; + home: HomePublicPluginSetup; + kibana_legacy: KibanaLegacySetup; +} + +export class DashboardPlugin implements Plugin { + private startDependencies: { + dataStart: DataStart; + npDataStart: NpDataStart; + savedObjectsClient: SavedObjectsClientContract; + embeddables: IEmbeddableStart; + navigation: NavigationStart; + share: SharePluginStart; + } | null = null; + + public setup( + core: CoreSetup, + { __LEGACY: { getAngularDependencies }, home, kibana_legacy }: DashboardPluginSetupDependencies + ) { + const app: App = { + id: '', + title: 'Dashboards', + mount: async ({ core: contextCore }, params) => { + if (this.startDependencies === null) { + throw new Error('not started yet'); + } + const { + dataStart, + savedObjectsClient, + embeddables, + navigation, + share, + npDataStart, + } = this.startDependencies; + const angularDependencies = await getAngularDependencies(); + const deps: RenderDeps = { + core: contextCore as LegacyCoreStart, + ...angularDependencies, + navigation, + dataStart, + share, + npDataStart, + indexPatterns: dataStart.indexPatterns.indexPatterns, + savedObjectsClient, + chrome: contextCore.chrome, + addBasePath: contextCore.http.basePath.prepend, + uiSettings: contextCore.uiSettings, + savedQueryService: dataStart.search.services.savedQueryService, + embeddables, + dashboardCapabilities: contextCore.application.capabilities.dashboard, + localStorage: new Storage(localStorage), + }; + const { renderApp } = await import('./application'); + return renderApp(params.element, params.appBasePath, deps); + }, + }; + kibana_legacy.registerLegacyApp({ ...app, id: 'dashboard' }); + kibana_legacy.registerLegacyApp({ ...app, id: 'dashboards' }); + + home.featureCatalogue.register({ + id: 'dashboard', + title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { + defaultMessage: 'Dashboard', + }), + description: i18n.translate('kbn.dashboard.featureCatalogue.dashboardDescription', { + defaultMessage: 'Display and share a collection of visualizations and saved searches.', + }), + icon: 'dashboardApp', + path: `/app/kibana#${DashboardConstants.LANDING_PAGE_PATH}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + + start( + { savedObjects: { client: savedObjectsClient } }: CoreStart, + { data: dataStart, embeddables, navigation, npData, share }: DashboardPluginStartDependencies + ) { + this.startDependencies = { + dataStart, + npDataStart: npData, + savedObjectsClient, + embeddables, + navigation, + share, + }; + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 5b24aa13f4f77..4c417ed2954d3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; export interface SavedObjectDashboard extends SavedObject { @@ -34,7 +34,7 @@ export interface SavedObjectDashboard extends SavedObject { // TODO: write a migration to rid of this, it's only around for bwc. uiStateJSON?: string; lastSavedTitle: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; destroy: () => void; refreshInterval?: RefreshInterval; getQuery(): Query; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js index 153a049276cee..aa7e219d75963 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js @@ -17,9 +17,16 @@ * under the License. */ + import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +jest.mock('../legacy_imports', () => ({ + SavedObjectSaveModal: () => null +})); + +jest.mock('ui/new_platform'); + import { DashboardSaveModal } from './save_modal'; test('renders DashboardSaveModal', () => { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx index 47455f04ba809..0640b2be431be 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx @@ -19,10 +19,10 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui'; +import { SavedObjectSaveModal } from '../legacy_imports'; + interface SaveOptions { newTitle: string; newDescription: string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx index c3cd5621b2c88..af1020e01e0c5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx @@ -17,10 +17,10 @@ * under the License. */ -import { I18nContext } from 'ui/i18n'; import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import { DashboardCloneModal } from './clone_modal'; export function showCloneModal( @@ -54,7 +54,7 @@ export function showCloneModal( }; document.body.appendChild(container); const element = ( - + - + ); ReactDOM.render(element, container); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx index 8640d7dbc6bdc..7c23e4808fbea 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx @@ -19,9 +19,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; - +import { I18nProvider } from '@kbn/i18n/react'; import { EuiWrappingPopover } from '@elastic/eui'; + import { OptionsMenu } from './options'; let isOpen = false; @@ -55,7 +55,7 @@ export function showOptionsPopover({ document.body.appendChild(container); const element = ( - + - + ); ReactDOM.render(element, container); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/types.ts index 3c2c87a502da4..371274401739e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/types.ts @@ -17,9 +17,8 @@ * under the License. */ -import { AppState } from 'ui/state_management/app_state'; -import { AppState as TAppState } from 'ui/state_management/app_state'; import { ViewMode } from 'src/plugins/embeddable/public'; +import { AppState } from './legacy_imports'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -153,5 +152,5 @@ export type AddFilterFn = ( operator: string; index: string; }, - appState: TAppState + appState: AppState ) => void; diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/README.md b/src/legacy/core_plugins/kibana/public/dev_tools/README.md new file mode 100644 index 0000000000000..0234830d6071c --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dev_tools/README.md @@ -0,0 +1,4 @@ +This folder is just a left-over of the things that can't be moved to Kibana platform just yet: + +* Styling (this can be moved as soon as there is support for styling in Kibana platform) +* Check whether there are no dev tools and hide the link in the nav bar (this can be moved as soon as all dev tools are moved) \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts index 74708e36a98aa..f2555259028cc 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts +++ b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts @@ -17,18 +17,13 @@ * under the License. */ -import { npSetup, npStart } from 'ui/new_platform'; +// make sure all dev tools are loaded and registered. +import 'uiExports/devTools'; -import { DevToolsPlugin } from './plugin'; -import { localApplicationService } from '../local_application_service'; +import { npStart } from 'ui/new_platform'; -const instance = new DevToolsPlugin(); - -instance.setup(npSetup.core, { - __LEGACY: { - localApplicationService, - }, -}); -instance.start(npStart.core, { - newPlatformDevTools: npStart.plugins.devTools, -}); +if (npStart.plugins.dev_tools.getSortedDevTools().length === 0) { + npStart.core.chrome.navLinks.update('kibana:dev_tools', { + hidden: true, + }); +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts b/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts deleted file mode 100644 index ec9af1a6acd92..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts +++ /dev/null @@ -1,71 +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. - */ - -// This import makes sure dev tools are registered before the app is. -import 'uiExports/devTools'; - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; - -import { LocalApplicationService } from '../local_application_service'; -import { DevTool, DevToolsStart } from '../../../../../plugins/dev_tools/public'; - -export interface DevToolsPluginSetupDependencies { - __LEGACY: { - localApplicationService: LocalApplicationService; - }; -} - -export interface DevToolsPluginStartDependencies { - newPlatformDevTools: DevToolsStart; -} - -export class DevToolsPlugin implements Plugin { - private getSortedDevTools: (() => readonly DevTool[]) | null = null; - - public setup( - core: CoreSetup, - { __LEGACY: { localApplicationService } }: DevToolsPluginSetupDependencies - ) { - localApplicationService.register({ - id: 'dev_tools', - title: 'Dev Tools', - mount: async (appMountContext, params) => { - if (!this.getSortedDevTools) { - throw new Error('not started yet'); - } - const { renderApp } = await import('./application'); - return renderApp( - params.element, - appMountContext, - params.appBasePath, - this.getSortedDevTools() - ); - }, - }); - } - - public start(core: CoreStart, { newPlatformDevTools }: DevToolsPluginStartDependencies) { - this.getSortedDevTools = newPlatformDevTools.getSortedDevTools; - if (this.getSortedDevTools().length === 0) { - core.chrome.navLinks.update('kibana:dev_tools', { - hidden: true, - }); - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index a5b55e50eb90e..bac56f008233c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -241,7 +241,6 @@ describe('discover field chooser directives', function () { $scope.computeDetails(field); expect(field.details.buckets).to.not.be(undefined); expect(field.details.buckets[0].value).to.be(40.141592); - expect(field.details.buckets[0].display).to.be('40.142'); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js index 46e66177b516a..4eb68c1bf50bc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js @@ -58,7 +58,7 @@ describe('context app', function () { .then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).to.be(true); - expect(setParentSpy.firstCall.args[0]).to.eql(false); + expect(setParentSpy.firstCall.args[0]).to.be(undefined); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js index 2bf3da42e24e5..ea6a8c092e242 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js @@ -196,7 +196,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js index b8bec40f2859c..486c8ed9b410e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js @@ -199,7 +199,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js index 62bbc6166662f..8c4cce810ca13 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js @@ -30,7 +30,7 @@ export function fetchAnchorProvider(indexPatterns) { ) { const indexPattern = await indexPatterns.get(indexPatternId); const searchSource = new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('version', true) .setField('size', 1) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts index 3314bbbf189c4..68ccf56594e72 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../ui/public/courier'; import { IndexPatterns, IndexPattern, getServices } from '../../../kibana_services'; -import { reverseSortDir, SortDirection } from './utils/sorting'; +import { reverseSortDir } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; @@ -114,7 +115,7 @@ function fetchContextProvider(indexPatterns: IndexPatterns) { async function createSearchSource(indexPattern: IndexPattern, filters: esFilters.Filter[]) { return new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts index eeae2aa2c5d0a..33f4454c18d40 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { reverseSortDir, SortDirection } from '../sorting'; +import { reverseSortDir } from '../sorting'; +import { SortDirection } from '../../../../../../../../../ui/public/courier'; + +jest.mock('ui/new_platform'); describe('function reverseSortDir', function() { test('reverse a given sort direction', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts index 2810e5d9d7e66..19c2ee2cdfe10 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { SearchSource } from '../../../../kibana_services'; +import { + EsQuerySortValue, + SortDirection, + SearchSourceContract, +} from '../../../../../../../../ui/public/courier'; import { convertTimeValueToIso } from './date_conversion'; -import { SortDirection } from './sorting'; import { EsHitRecordList } from '../context'; import { IntervalValue } from './generate_intervals'; -import { EsQuerySort } from './get_es_query_sort'; import { EsQuerySearchAfter } from './get_es_query_search_after'; interface RangeQuery { @@ -38,9 +40,9 @@ interface RangeQuery { * and filters set. */ export async function fetchHitsInInterval( - searchSource: SearchSource, + searchSource: SearchSourceContract, timeField: string, - sort: EsQuerySort, + sort: [EsQuerySortValue, EsQuerySortValue], sortDir: SortDirection, interval: IntervalValue[], searchAfter: EsQuerySearchAfter, diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts index a50764fe542b1..cb4878239ff92 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; +import { SortDirection } from '../../../../../../../../ui/public/courier'; export type IntervalValue = number | null; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts index c9f9b9b939f3d..39c69112e58cb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts @@ -16,11 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; -type EsQuerySortValue = Record; - -export type EsQuerySort = [EsQuerySortValue, EsQuerySortValue]; +import { EsQuerySortValue, SortDirection } from '../../../../../../../../ui/public/courier/types'; /** * Returns `EsQuerySort` which is used to sort records in the ES query @@ -33,6 +30,6 @@ export function getEsQuerySort( timeField: string, tieBreakerField: string, sortDir: SortDirection -): EsQuerySort { +): [EsQuerySortValue, EsQuerySortValue] { return [{ [timeField]: sortDir }, { [tieBreakerField]: sortDir }]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts index 4a0f531845f46..47385aecb1937 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts @@ -17,13 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../../ui/public/courier'; import { IndexPattern } from '../../../../kibana_services'; -export enum SortDirection { - asc = 'asc', - desc = 'desc', -} - /** * The list of field names that are allowed for sorting, but not included in * index pattern fields. diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context_app.html b/src/legacy/core_plugins/kibana/public/discover/angular/context_app.html index 68e1d536a91ce..3e0f8a8329154 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context_app.html +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context_app.html @@ -1,9 +1,16 @@ - +> + +
$injector.invoke( @@ -119,50 +119,53 @@ uiRoutes template: indexTemplate, reloadOnSearch: false, resolve: { - ip: function (Promise, indexPatterns, config, Private) { + savedObjects: function (Promise, indexPatterns, config, Private, $rootScope, kbnUrl, redirectWhenMissing, savedSearches, $route) { const State = Private(StateProvider); - return indexPatterns.getCache().then((savedObjects)=> { - /** - * In making the indexPattern modifiable it was placed in appState. Unfortunately, - * the load order of AppState conflicts with the load order of many other things - * so in order to get the name of the index we should use, and to switch to the - * default if necessary, we parse the appState with a temporary State object and - * then destroy it immediatly after we're done - * - * @type {State} - */ - const state = new State('_a', {}); - - const specified = !!state.index; - const exists = _.findIndex(savedObjects, o => o.id === state.index) > -1; - const id = exists ? state.index : config.get('defaultIndex'); - state.destroy(); + const savedSearchId = $route.current.params.id; + return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => { return Promise.props({ - list: savedObjects, - loaded: indexPatterns.get(id), - stateVal: state.index, - stateValFound: specified && exists + ip: indexPatterns.getCache().then((savedObjects) => { + /** + * In making the indexPattern modifiable it was placed in appState. Unfortunately, + * the load order of AppState conflicts with the load order of many other things + * so in order to get the name of the index we should use, and to switch to the + * default if necessary, we parse the appState with a temporary State object and + * then destroy it immediatly after we're done + * + * @type {State} + */ + const state = new State('_a', {}); + + const specified = !!state.index; + const exists = _.findIndex(savedObjects, o => o.id === state.index) > -1; + const id = exists ? state.index : config.get('defaultIndex'); + state.destroy(); + + return Promise.props({ + list: savedObjects, + loaded: indexPatterns.get(id), + stateVal: state.index, + stateValFound: specified && exists + }); + }), + savedSearch: savedSearches.get(savedSearchId) + .then((savedSearch) => { + if (savedSearchId) { + chrome.recentlyAccessed.add( + savedSearch.getFullPath(), + savedSearch.title, + savedSearchId); + } + return savedSearch; + }) + .catch(redirectWhenMissing({ + 'search': '/discover', + 'index-pattern': '/management/kibana/objects/savedSearches/' + $route.current.params.id + })) }); }); }, - savedSearch: function (redirectWhenMissing, savedSearches, $route) { - const savedSearchId = $route.current.params.id; - return savedSearches.get(savedSearchId) - .then((savedSearch) => { - if (savedSearchId) { - chrome.recentlyAccessed.add( - savedSearch.getFullPath(), - savedSearch.title, - savedSearchId); - } - return savedSearch; - }) - .catch(redirectWhenMissing({ - 'search': '/discover', - 'index-pattern': '/management/kibana/objects/savedSearches/' + $route.current.params.id - })); - } } }); @@ -224,7 +227,7 @@ function discoverController( }; // the saved savedSearch - const savedSearch = $route.current.locals.savedSearch; + const savedSearch = $route.current.locals.savedObjects.savedSearch; let abortController; $scope.$on('$destroy', () => { @@ -417,20 +420,6 @@ function discoverController( queryFilter.setFilters(filters); }; - $scope.applyFilters = filters => { - const { timeRangeFilter, restOfFilters } = extractTimeFilter($scope.indexPattern.timeFieldName, filters); - queryFilter.addFilters(restOfFilters); - if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter); - - $scope.state.$newFilters = []; - }; - - $scope.$watch('state.$newFilters', (filters = []) => { - if (filters.length === 1) { - $scope.applyFilters(filters); - } - }); - const getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding @@ -539,7 +528,7 @@ function discoverController( sampleSize: config.get('discover:sampleSize'), timefield: isDefaultTypeIndexPattern($scope.indexPattern) && $scope.indexPattern.timeFieldName, savedSearch: savedSearch, - indexPatternList: $route.current.locals.ip.list, + indexPatternList: $route.current.locals.savedObjects.ip.list, }; const shouldSearchOnPageLoad = () => { @@ -1055,7 +1044,7 @@ function discoverController( loaded: loadedIndexPattern, stateVal, stateValFound, - } = $route.current.locals.ip; + } = $route.current.locals.savedObjects.ip; const ownIndexPattern = $scope.searchSource.getOwnField('index'); @@ -1103,12 +1092,12 @@ function discoverController( // Block the UI from loading if the user has loaded a rollup index pattern but it isn't // supported. $scope.isUnsupportedIndexPattern = ( - !isDefaultTypeIndexPattern($route.current.locals.ip.loaded) - && !hasSearchStategyForIndexPattern($route.current.locals.ip.loaded) + !isDefaultTypeIndexPattern($route.current.locals.savedObjects.ip.loaded) + && !hasSearchStategyForIndexPattern($route.current.locals.savedObjects.ip.loaded) ); if ($scope.isUnsupportedIndexPattern) { - $scope.unsupportedIndexPatternType = $route.current.locals.ip.loaded.type; + $scope.unsupportedIndexPatternType = $route.current.locals.savedObjects.ip.loaded.type; return; } diff --git a/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts b/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts index 51e0dcba1cad0..6c3856932c96c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts +++ b/src/legacy/core_plugins/kibana/public/discover/breadcrumbs.ts @@ -34,7 +34,7 @@ export function getSavedSearchBreadcrumbs($route: any) { return [ ...getRootBreadcrumbs(), { - text: $route.current.locals.savedSearch.id, + text: $route.current.locals.savedObjects.savedSearch.id, }, ]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js deleted file mode 100644 index ad68e55e71622..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu.js +++ /dev/null @@ -1,43 +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, { Fragment, PureComponent } from 'react'; -import { EuiButton, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getServices } from '../../kibana_services'; -const { docLinks } = getServices(); - -export class HelpMenu extends PureComponent { - render() { - return ( - - - - - - - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js index 58a92193de63e..eb40130137e00 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/help_menu/help_menu_util.js @@ -17,15 +17,20 @@ * under the License. */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { HelpMenu } from './help_menu'; +import { i18n } from '@kbn/i18n'; +import { getServices } from '../../kibana_services'; +const { docLinks } = getServices(); export function addHelpMenuToAppChrome(chrome) { - chrome.setHelpExtension(domElement => { - render(, domElement); - return () => { - unmountComponentAtNode(domElement); - }; + chrome.setHelpExtension({ + appName: i18n.translate('kbn.discover.helpMenu.appName', { + defaultMessage: 'Discover', + }), + links: [ + { + linkType: 'documentation', + href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/discover.html`, + }, + ], }); } diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index ef79cda476e51..9fee0cfc3ea00 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, TimeRange, @@ -51,7 +52,6 @@ import { getServices, IndexPattern, RequestAdapter, - SearchSource, } from '../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; @@ -92,7 +92,7 @@ export class SearchEmbeddable extends Embeddable private inspectorAdaptors: Adapters; private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: SearchSource; + private filtersSearchSource?: SearchSourceContract; private searchInstance?: JQLite; private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; @@ -194,13 +194,11 @@ export class SearchEmbeddable extends Embeddable searchScope.inspectorAdapters = this.inspectorAdaptors; const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index')); + const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) { - return; - } + if (!this.searchScope || !this.input.timeRange) return; return getTime(indexPattern, this.input.timeRange); }); @@ -241,7 +239,7 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id); + let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id!); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 1939cc7060621..ebea646a09889 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -84,6 +84,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< const queryFilter = Private(getServices().FilterBarQueryFilterProvider); try { const savedObject = await searchLoader.get(savedObjectId); + const indexPattern = savedObject.searchSource.getField('index'); return new SearchEmbeddable( { savedSearch: savedObject, @@ -92,7 +93,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< editUrl, queryFilter, editable: getServices().capabilities.discover.save as boolean, - indexPatterns: _.compact([savedObject.searchSource.getField('index')]), + indexPatterns: indexPattern ? [indexPattern] : [], }, input, this.executeTriggerActions, diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 61d7933464e7f..fc5f34fab7564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -28,7 +28,6 @@ import angular from 'angular'; // just used in embeddables and discover controll import uiRoutes from 'ui/routes'; // @ts-ignore import { uiModules } from 'ui/modules'; -import { SearchSource } from 'ui/courier'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; // @ts-ignore @@ -43,9 +42,11 @@ import { wrapInI18nContext } from 'ui/i18n'; import { docTitle } from 'ui/doc_title'; // @ts-ignore import * as docViewsRegistry from 'ui/registry/doc_views'; +import { SearchSource } from '../../../../ui/public/courier'; const services = { // new plattform + core: npStart.core, addBasePath: npStart.core.http.basePath.prepend, capabilities: npStart.core.application.capabilities, chrome: npStart.core.chrome, @@ -86,9 +87,10 @@ export { callAfterBindingsWorkaround } from 'ui/compat'; export { getRequestInspectorStats, getResponseInspectorStats, -} from 'ui/courier/utils/courier_inspector_utils'; -// @ts-ignore -export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; + hasSearchStategyForIndexPattern, + isDefaultTypeIndexPattern, + SearchSource, +} from '../../../../ui/public/courier'; // @ts-ignore export { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; // @ts-ignore @@ -108,12 +110,12 @@ export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; export { tabifyAggResponse } from 'ui/agg_response/tabify'; // @ts-ignore export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; +export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; export { unhashUrl } from 'ui/state_management/state_hashing'; // EXPORT types export { Vis } from 'ui/vis'; export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; -export { SearchSource } from 'ui/courier'; export { ElasticSearchHit } from 'ui/registry/doc_views_types'; export { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; export { Adapters } from 'ui/inspector/types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 873c429bf705d..7c2fb4f118915 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -21,10 +21,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/p import { IUiActionsStart } from 'src/plugins/ui_actions/public'; import { registerFeature } from './helpers/register_feature'; import './kibana_services'; -import { - Start as EmbeddableStart, - Setup as EmbeddableSetup, -} from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; /** * These are the interfaces with your public contracts. You should export these @@ -35,11 +32,11 @@ export type DiscoverSetup = void; export type DiscoverStart = void; interface DiscoverSetupPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; } interface DiscoverStartPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; } export class DiscoverPlugin implements Plugin { diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index 7d8740243ec02..6cdd802fa2800 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -17,13 +17,14 @@ * under the License. */ -import { SearchSource } from './kibana_services'; +import { SearchSourceContract } from '../../../../ui/public/courier'; import { SortOrder } from './angular/doc_table/components/table_header/helpers'; +export { SortOrder } from './angular/doc_table/components/table_header/helpers'; export interface SavedSearch { readonly id: string; title: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; description?: string; columns: string[]; sort: SortOrder[]; diff --git a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js b/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js deleted file mode 100644 index 1c63d2efc7e0b..0000000000000 --- a/src/legacy/core_plugins/kibana/public/field_formats/__tests__/_conformance.js +++ /dev/null @@ -1,142 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { fieldFormats } from 'ui/registry/field_formats'; -import { npStart } from 'ui/new_platform'; -import { FieldFormat } from '../../../../../../plugins/data/public'; - -const config = npStart.core.uiSettings; - -const formatIds = [ - 'bytes', - 'date', - 'date_nanos', - 'duration', - 'ip', - 'number', - 'percent', - 'color', - 'string', - 'url', - '_source', - 'truncate', - 'boolean', - 'relative_date', - 'static_lookup' -]; - -// eslint-disable-next-line import/no-default-export -export default describe('conformance', function () { - - const getConfig = (...args) => config.get(...args); - - formatIds.forEach(function (id) { - let instance; - let Type; - - beforeEach(function () { - Type = fieldFormats.getType(id); - instance = fieldFormats.getInstance(id); - }); - - describe(id + ' Type', function () { - it('has an id', function () { - expect(Type.id).to.be.a('string'); - }); - - it('has a title', function () { - expect(Type.title).to.be.a('string'); - }); - - it('declares compatible field formats as a string or array', function () { - expect(Type.fieldType).to.be.ok(); - expect(_.isString(Type.fieldType) || Array.isArray(Type.fieldType)).to.be(true); - }); - }); - - describe(id + ' Instance', function () { - it('extends FieldFormat', function () { - expect(instance).to.be.a(FieldFormat); - }); - }); - }); - - it('registers all of the fieldFormats', function () { - expect(_.difference(fieldFormats.raw, formatIds.map(fieldFormats.getType))).to.eql([]); - }); - - describe('Bytes format', basicPatternTests('bytes', require('numeral'))); - describe('Percent Format', basicPatternTests('percent', require('numeral'))); - describe('Date Format', basicPatternTests('date', require('moment'))); - - describe('Number Format', function () { - basicPatternTests('number', require('numeral'))(); - - it('tries to parse strings', function () { - const number = new (fieldFormats.getType('number'))({ pattern: '0.0b' }, getConfig); - expect(number.convert(123.456)).to.be('123.5B'); - expect(number.convert('123.456')).to.be('123.5B'); - }); - - }); - - function basicPatternTests(id, lib) { - const confKey = id === 'date' ? 'dateFormat' : 'format:' + id + ':defaultPattern'; - - return function () { - it('converts using the format:' + id + ':defaultPattern config', function () { - const inst = fieldFormats.getInstance(id); - [ - '0b', - '0 b', - '0.[000] b', - '0.[000]b', - '0.[0]b' - ].forEach(function (pattern) { - const original = config.get(confKey); - const num = _.random(-10000, 10000, true); - config.set(confKey, pattern); - expect(inst.convert(num)).to.be(lib(num).format(pattern)); - config.set(confKey, original); - }); - }); - - it('uses the pattern param if available', function () { - const original = config.get(confKey); - const num = _.random(-10000, 10000, true); - const defFormat = '0b'; - const customFormat = '0.00000%'; - - config.set(confKey, defFormat); - const defInst = fieldFormats.getInstance(id); - - const Type = fieldFormats.getType(id); - const customInst = new Type({ pattern: customFormat }, getConfig); - - expect(defInst.convert(num)).to.not.be(customInst.convert(num)); - expect(defInst.convert(num)).to.be(lib(num).format(defFormat)); - expect(customInst.convert(num)).to.be(lib(num).format(customFormat)); - - config.set(confKey, original); - }); - }; - } -}); diff --git a/src/legacy/core_plugins/kibana/public/field_formats/register.js b/src/legacy/core_plugins/kibana/public/field_formats/register.js deleted file mode 100644 index 9709b56fc8c3c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/field_formats/register.js +++ /dev/null @@ -1,53 +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 { fieldFormats } from 'ui/registry/field_formats'; -import { - UrlFormat, - StringFormat, - NumberFormat, - BytesFormat, - TruncateFormat, - RelativeDateFormat, - PercentFormat, - IpFormat, - DurationFormat, - DateNanosFormat, - DateFormat, - ColorFormat, - BoolFormat, - SourceFormat, - StaticLookupFormat -} from '../../../../../plugins/data/public'; - -fieldFormats.register(UrlFormat); -fieldFormats.register(BytesFormat); -fieldFormats.register(DateFormat); -fieldFormats.register(DateNanosFormat); -fieldFormats.register(RelativeDateFormat); -fieldFormats.register(DurationFormat); -fieldFormats.register(IpFormat); -fieldFormats.register(NumberFormat); -fieldFormats.register(PercentFormat); -fieldFormats.register(StringFormat); -fieldFormats.register(SourceFormat); -fieldFormats.register(ColorFormat); -fieldFormats.register(TruncateFormat); -fieldFormats.register(BoolFormat); -fieldFormats.register(StaticLookupFormat); diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap index bbcb2096b6f64..35a7216df8bc6 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/add_data.test.js.snap @@ -67,10 +67,7 @@ exports[`apmUiEnabled 1`] = ` type="logoAPM" /> } - layout="vertical" - textAlign="center" title="APM" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Logs" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Metrics" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="SIEM" - titleElement="span" /> @@ -332,10 +320,7 @@ exports[`isNewKibanaInstance 1`] = ` type="logoLogging" /> } - layout="vertical" - textAlign="center" title="Logs" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Metrics" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="SIEM" - titleElement="span" /> @@ -560,10 +539,7 @@ exports[`mlEnabled 1`] = ` type="logoAPM" /> } - layout="vertical" - textAlign="center" title="APM" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Logs" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Metrics" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="SIEM" - titleElement="span" /> @@ -861,10 +828,7 @@ exports[`render 1`] = ` type="logoLogging" /> } - layout="vertical" - textAlign="center" title="Logs" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="Metrics" - titleElement="span" /> } - layout="vertical" - textAlign="center" title="SIEM" - titleElement="span" /> diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/synopsis.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/synopsis.test.js.snap index 1970d048be4fd..34cc0eb9265ff 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/synopsis.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/synopsis.test.js.snap @@ -14,9 +14,7 @@ exports[`props iconType 1`] = ` /> } layout="horizontal" - textAlign="center" title="Great tutorial" - titleElement="span" /> `; @@ -35,9 +33,7 @@ exports[`props iconUrl 1`] = ` /> } layout="horizontal" - textAlign="center" title="Great tutorial" - titleElement="span" /> `; @@ -49,9 +45,7 @@ exports[`props isBeta 1`] = ` description="this is a great tutorial about..." href="link_to_item" layout="horizontal" - textAlign="center" title="Great tutorial" - titleElement="span" /> `; @@ -63,8 +57,6 @@ exports[`render 1`] = ` description="this is a great tutorial about..." href="link_to_item" layout="horizontal" - textAlign="center" title="Great tutorial" - titleElement="span" /> `; diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index 4ebf719b86233..6494cc79640e1 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -25,7 +25,6 @@ import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { start as data } from '../../../data/public/legacy'; import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -import { localApplicationService } from '../local_application_service'; export const trackUiMetric = createUiStatsReporter('Kibana_home'); @@ -54,6 +53,7 @@ let copiedLegacyCatalogue = false; (async () => { const instance = new HomePlugin(); instance.setup(npSetup.core, { + ...npSetup.plugins, __LEGACY: { trackUiMetric, metadata: npStart.core.injectedMetadata.getLegacyMetadata(), @@ -64,14 +64,13 @@ let copiedLegacyCatalogue = false; const Private = injector.get('Private'); // Merge legacy registry with new registry (Private(FeatureCatalogueRegistryProvider as any) as any).inTitleOrder.map( - npSetup.plugins.feature_catalogue.register + npSetup.plugins.home.featureCatalogue.register ); copiedLegacyCatalogue = true; } - return npStart.plugins.feature_catalogue.get(); + return npStart.plugins.home.featureCatalogue.get(); }, getAngularDependencies, - localApplicationService, }, }); instance.start(npStart.core, { diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 6189204ee4cfc..5ef6e019db042 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -29,7 +29,7 @@ import { UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; -import { FeatureCatalogueEntry } from '../../../../../plugins/feature_catalogue/public'; +import { FeatureCatalogueEntry } from '../../../../../plugins/home/public'; export interface HomeKibanaServices { indexPatternService: any; diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 2a2ea371d7f3b..bb0e7e3611616 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -21,9 +21,9 @@ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'ki import { UiStatsMetricType } from '@kbn/analytics'; import { DataStart } from '../../../data/public'; -import { LocalApplicationService } from '../local_application_service'; import { setServices } from './kibana_services'; -import { FeatureCatalogueEntry } from '../../../../../plugins/feature_catalogue/public'; +import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { FeatureCatalogueEntry } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { telemetryOptInProvider: any; @@ -53,8 +53,8 @@ export interface HomePluginSetupDependencies { }; getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; - localApplicationService: LocalApplicationService; }; + kibana_legacy: KibanaLegacySetup; } export class HomePlugin implements Plugin { @@ -64,10 +64,11 @@ export class HomePlugin implements Plugin { setup( core: CoreSetup, { - __LEGACY: { localApplicationService, getAngularDependencies, ...legacyServices }, + kibana_legacy, + __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { - localApplicationService.register({ + kibana_legacy.registerLegacyApp({ id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png new file mode 100644 index 0000000000000..fc895ceab20ba Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg new file mode 100644 index 0000000000000..20694ba6e62c7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 14fc2ec6ead00..98def2252b75c 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -31,7 +31,6 @@ import 'uiExports/visTypes'; import 'uiExports/visEditorTypes'; import 'uiExports/visualize'; import 'uiExports/savedObjectTypes'; -import 'uiExports/fieldFormats'; import 'uiExports/fieldFormatEditors'; import 'uiExports/navbarExtensions'; import 'uiExports/contextMenuActions'; diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index 9d87e187fd1e1..e5bfd88ea7637 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -23,12 +23,6 @@ import { ILocationService, IScope } from 'angular'; import { npStart } from 'ui/new_platform'; import { htmlIdGenerator } from '@elastic/eui'; -interface ForwardDefinition { - legacyAppId: string; - newAppId: string; - keepPrefix: boolean; -} - const matchAllWithPrefix = (prefixOrApp: string | App) => `/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}/:tail*?`; @@ -45,55 +39,8 @@ const matchAllWithPrefix = (prefixOrApp: string | App) => * router that handles switching between applications without page reload. */ export class LocalApplicationService { - private apps: App[] = []; - private forwards: ForwardDefinition[] = []; private idGenerator = htmlIdGenerator('kibanaAppLocalApp'); - /** - * Register an app to be managed by the application service. - * This method works exactly as `core.application.register`. - * - * When an app is mounted, it is responsible for routing. The app - * won't be mounted again if the route changes within the prefix - * of the app (its id). It is fine to use whatever means for handling - * routing within the app. - * - * When switching to a URL outside of the current prefix, the app router - * shouldn't do anything because it doesn't own the routing anymore - - * the local application service takes over routing again, - * unmounts the current app and mounts the next app. - * - * @param app The app descriptor - */ - register(app: App) { - this.apps.push(app); - } - - /** - * Forwards every URL starting with `legacyAppId` to the same URL starting - * with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to - * `/newApp/my/legacy/path?q=123`. - * - * When setting the `keepPrefix` option, the new app id is simply prepended. - * The example above would become `/newApp/legacy/my/legacy/path?q=123`. - * - * This method can be used to provide backwards compatibility for URLs when - * renaming or nesting plugins. For route changes after the prefix, please - * use the routing mechanism of your app. - * - * @param legacyAppId The name of the old app to forward URLs from - * @param newAppId The name of the new app that handles the URLs now - * @param options Whether the prefix of the old app is kept to nest the legacy - * path into the new path - */ - forwardApp( - legacyAppId: string, - newAppId: string, - options: { keepPrefix: boolean } = { keepPrefix: false } - ) { - this.forwards.push({ legacyAppId, newAppId, ...options }); - } - /** * Wires up listeners to handle mounting and unmounting of apps to * the legacy angular route manager. Once all apps within the Kibana @@ -103,7 +50,7 @@ export class LocalApplicationService { * @param angularRouteManager The current `ui/routes` instance */ attachToAngular(angularRouteManager: UIRoutes) { - this.apps.forEach(app => { + npStart.plugins.kibana_legacy.getApps().forEach(app => { const wrapperElementId = this.idGenerator(); angularRouteManager.when(matchAllWithPrefix(app), { outerAngularWrapperRoute: true, @@ -131,7 +78,7 @@ export class LocalApplicationService { }); }); - this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => { + npStart.plugins.kibana_legacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { angularRouteManager.when(matchAllWithPrefix(legacyAppId), { resolveRedirectTo: ($location: ILocationService) => { const url = $location.url(); diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index c0949318e9253..83fc8e4db9b55 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -28,7 +28,6 @@ import { I18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; import appTemplate from './app.html'; import landingTemplate from './landing.html'; -import { capabilities } from 'ui/capabilities'; import { management, SidebarNav, MANAGEMENT_BREADCRUMB } from 'ui/management'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { timefilter } from 'ui/timefilter'; @@ -50,13 +49,6 @@ uiRoutes redirectTo: '/management' }); -require('./route_setup/load_default')({ - whenMissingRedirectTo: () => { - const canManageIndexPatterns = capabilities.get().management.kibana.index_patterns; - return canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; - } -}); - export function updateLandingPage(version) { const node = document.getElementById(LANDING_ID); if (!node) { diff --git a/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js b/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js deleted file mode 100644 index f797acbe8888e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js +++ /dev/null @@ -1,110 +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 _ from 'lodash'; -import React from 'react'; -import { banners } from 'ui/notify'; -import { NoDefaultIndexPattern } from 'ui/index_patterns'; -import uiRoutes from 'ui/routes'; -import { - EuiCallOut, -} from '@elastic/eui'; -import { clearTimeout } from 'timers'; -import { i18n } from '@kbn/i18n'; - -let bannerId; -let timeoutId; - -function displayBanner() { - clearTimeout(timeoutId); - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = banners.set({ - id: bannerId, // initially undefined, but reused after first set - component: ( - - ) - }); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - banners.remove(bannerId); - timeoutId = undefined; - }, 15000); -} - -// eslint-disable-next-line import/no-default-export -export default function (opts) { - opts = opts || {}; - const whenMissingRedirectTo = opts.whenMissingRedirectTo || null; - - uiRoutes - .addSetupWork(function loadDefaultIndexPattern(Promise, $route, config, indexPatterns) { - const route = _.get($route, 'current.$$route'); - - if (!route.requireDefaultIndex) { - return; - } - - return indexPatterns.getIds() - .then(function (patterns) { - let defaultId = config.get('defaultIndex'); - let defined = !!defaultId; - const exists = _.contains(patterns, defaultId); - - if (defined && !exists) { - config.remove('defaultIndex'); - defaultId = defined = false; - } - - if (!defined) { - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { - defaultId = patterns[0]; - config.set('defaultIndex', defaultId); - } else { - throw new NoDefaultIndexPattern(); - } - } - }); - }) - .afterWork( - // success - null, - - // failure - function (err, kbnUrl) { - const hasDefault = !(err instanceof NoDefaultIndexPattern); - if (hasDefault || !whenMissingRedirectTo) throw err; // rethrow - - kbnUrl.change(whenMissingRedirectTo()); - - displayBanner(); - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html index 0ef3cce832bc7..bf9ac9b9bbe36 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html @@ -1,7 +1,7 @@
@@ -33,6 +33,7 @@ show-query-bar is set to "true". -->
savedVisualizations.get($route.current.params)) .then(savedVis => { if (savedVis.vis.type.setup) { return savedVis.vis.type.setup(savedVis) @@ -102,28 +104,33 @@ uiRoutes template: editorTemplate, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - savedVis: function (savedVisualizations, redirectWhenMissing, $route) { - return savedVisualizations.get($route.current.params.id) + savedVis: function (savedVisualizations, redirectWhenMissing, $route, $rootScope, kbnUrl) { + return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) + .then(() => savedVisualizations.get($route.current.params.id)) .then((savedVis) => { chrome.recentlyAccessed.add( savedVis.getFullPath(), savedVis.title, - savedVis.id); + savedVis.id + ); return savedVis; }) .then(savedVis => { if (savedVis.vis.type.setup) { - return savedVis.vis.type.setup(savedVis) - .catch(() => savedVis); + return savedVis.vis.type.setup(savedVis).catch(() => savedVis); } return savedVis; }) - .catch(redirectWhenMissing({ - 'visualization': '/visualize', - 'search': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern-field': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id - })); + .catch( + redirectWhenMissing({ + visualization: '/visualize', + search: '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern-field': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + }) + ); } } }); @@ -418,6 +425,12 @@ function VisEditor( next: updateTimeRange })); + subscriptions.add(chrome.getIsVisible$().subscribe(isVisible => { + $scope.$evalAsync(() => { + $scope.isVisible = isVisible; + }); + })); + // update the searchSource when query updates $scope.fetch = function () { $state.save(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 924f72594ad34..a2b46dab1ef33 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -25,12 +25,12 @@ import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/saved_object'; import { Vis } from 'ui/vis'; -import { SearchSource } from 'ui/courier'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { TimeRange, @@ -53,7 +53,7 @@ const getKeys = (o: T): Array => Object.keys(o) as Array< export interface VisSavedObject extends SavedObject { vis: Vis; description?: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; title: string; uiStateJSON?: string; destroy: () => void; diff --git a/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu.js b/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu.js deleted file mode 100644 index 40a1b79ea3520..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu.js +++ /dev/null @@ -1,45 +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, { Fragment, PureComponent } from 'react'; -import { EuiButton, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { getServices } from '../kibana_services'; - -const { docLinks } = getServices(); - -export class HelpMenu extends PureComponent { - render() { - return ( - - - - - - - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu_util.js b/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu_util.js index 58a92193de63e..d27003f39d4c0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu_util.js +++ b/src/legacy/core_plugins/kibana/public/visualize/help_menu/help_menu_util.js @@ -17,15 +17,20 @@ * under the License. */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { HelpMenu } from './help_menu'; +import { i18n } from '@kbn/i18n'; +import { getServices } from '../kibana_services'; +const { docLinks } = getServices(); export function addHelpMenuToAppChrome(chrome) { - chrome.setHelpExtension(domElement => { - render(, domElement); - return () => { - unmountComponentAtNode(domElement); - }; + chrome.setHelpExtension({ + appName: i18n.translate('kbn.visualize.helpMenu.appName', { + defaultMessage: 'Visualize', + }), + links: [ + { + linkType: 'documentation', + href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/visualize.html`, + }, + ], }); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.js b/src/legacy/core_plugins/kibana/public/visualize/index.js index 592a355a71b0d..57707f6321376 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/index.js @@ -17,6 +17,7 @@ * under the License. */ +import { ensureDefaultIndexPattern } from 'ui/legacy_compat'; import './editor/editor'; import { i18n } from '@kbn/i18n'; import './saved_visualizations/_saved_vis'; @@ -32,7 +33,6 @@ const { FeatureCatalogueRegistryProvider, uiRoutes } = getServices(); uiRoutes .defaults(/visualize/, { - requireDefaultIndex: true, requireUICapability: 'visualize.show', badge: uiCapabilities => { if (uiCapabilities.visualize.save) { @@ -57,6 +57,7 @@ uiRoutes controllerAs: 'listingController', resolve: { createNewVis: () => false, + hasDefaultIndex: ($rootScope, kbnUrl) => ensureDefaultIndexPattern(getServices().core, getServices().data, $rootScope, kbnUrl) }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -66,6 +67,7 @@ uiRoutes controllerAs: 'listingController', resolve: { createNewVis: () => true, + hasDefaultIndex: ($rootScope, kbnUrl) => ensureDefaultIndexPattern(getServices().core, getServices().data, $rootScope, kbnUrl) }, }); 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 3be49971cf4c9..e2201cdca9a57 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -60,6 +60,7 @@ const services = { savedObjectsClient: npStart.core.savedObjects.client, toastNotifications: npStart.core.notifications.toasts, uiSettings: npStart.core.uiSettings, + core: npStart.core, share: npStart.plugins.share, data, diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts index 3ff39c1a4eb8c..9890aaf187a13 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts @@ -19,6 +19,7 @@ import { Server } from 'hapi'; import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export function createCspCollector(server: Server) { return { @@ -42,8 +43,7 @@ export function createCspCollector(server: Server) { }; } -export function registerCspCollector(server: Server): void { - const { collectorSet } = server.usage; - const collector = collectorSet.makeUsageCollector(createCspCollector(server)); - collectorSet.register(collector); +export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void { + const collector = usageCollection.makeUsageCollector(createCspCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js index 19fb64b7ecc74..6d751a9e9ff45 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js @@ -19,14 +19,14 @@ import { fetchProvider } from './fetch'; -export function makeKQLUsageCollector(server) { +export function makeKQLUsageCollector(usageCollection, server) { const index = server.config().get('kibana.index'); const fetch = fetchProvider(index); - const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({ + const kqlUsageCollector = usageCollection.makeUsageCollector({ type: 'kql', fetch, isReady: () => true, }); - server.usage.collectorSet.register(kqlUsageCollector); + usageCollection.registerCollector(kqlUsageCollector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js index 24f336043d0d1..7737a0fbc2a71 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js @@ -20,29 +20,30 @@ import { makeKQLUsageCollector } from './make_kql_usage_collector'; describe('makeKQLUsageCollector', () => { - let server; let makeUsageCollectorStub; let registerStub; + let usageCollection; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = { - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - }, config: () => ({ get: () => '.kibana' }) }; }); - it('should call collectorSet.register', () => { - makeKQLUsageCollector(server); + it('should call registerCollector', () => { + makeKQLUsageCollector(usageCollection, server); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = kql', () => { - makeKQLUsageCollector(server); + makeKQLUsageCollector(usageCollection, server); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('kql'); }); diff --git a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js new file mode 100644 index 0000000000000..b76a9ee7c4dbe --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function activemqMetricsSpecProvider(context) { + const moduleName = 'activemq'; + return { + id: 'activemqMetrics', + name: i18n.translate('kbn.server.tutorials.activemqMetrics.nameTitle', { + defaultMessage: 'ActiveMQ metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + }), + longDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.longDescription', { + defaultMessage: 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + isBeta: true, + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.corednsMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/kibana#/discover' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-activemq.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json index f54c2fa35f80d..69a165c09c2f9 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json @@ -1,11 +1,11 @@ { "attributes": { - "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, "id": "apm-*", "type": "index-pattern", "version": "1" -} +} \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js new file mode 100644 index 0000000000000..f85dd63449af8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; + +export function awsLogsSpecProvider(server, context) { + const moduleName = 'aws'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; + return { + id: 'awsLogs', + name: i18n.translate('kbn.server.tutorials.awsLogs.nameTitle', { + defaultMessage: 'AWS S3 based logs', + }), + category: TUTORIAL_CATEGORY.LOGGING, + shortDescription: i18n.translate('kbn.server.tutorials.awsLogs.shortDescription', { + defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.', + }), + longDescription: i18n.translate('kbn.server.tutorials.awsLogs.longDescription', { + defaultMessage: 'Collect AWS logs by exporting them to an S3 bucket which is configured with SQS notification. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-aws.html', + }, + }), + euiIconType: 'logoAWS', + artifacts: { + dashboards: [ + { + id: '4746e000-bacd-11e9-9f70-1f7bda85a5eb', + linkLabel: i18n.translate('kbn.server.tutorials.awsLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'AWS S3 server access log dashboard', + }), + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-aws.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/aws_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js index 7617f313b2436..34bb168d5ab24 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js @@ -25,11 +25,11 @@ export function cloudwatchLogsSpecProvider(context) { return { id: 'cloudwatchLogs', name: i18n.translate('kbn.server.tutorials.cloudwatchLogs.nameTitle', { - defaultMessage: 'Cloudwatch Logs', + defaultMessage: 'AWS Cloudwatch logs', }), category: TUTORIAL_CATEGORY.LOGGING, shortDescription: i18n.translate('kbn.server.tutorials.cloudwatchLogs.shortDescription', { - defaultMessage: 'Collect Cloudwatch logs with Functionbeat', + defaultMessage: 'Collect Cloudwatch logs with Functionbeat.', }), longDescription: i18n.translate('kbn.server.tutorials.cloudwatchLogs.longDescription', { defaultMessage: 'Collect Cloudwatch logs by deploying Functionbeat to run as \ @@ -39,7 +39,7 @@ export function cloudwatchLogsSpecProvider(context) { learnMoreLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', }, }), - //euiIconType: 'functionbeatApp', + euiIconType: 'logoAWS', artifacts: { dashboards: [ // TODO diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 155d730ec3ede..f36909e59f39b 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -79,6 +79,8 @@ import { emsBoundariesSpecProvider } from './ems'; import { consulMetricsSpecProvider } from './consul_metrics'; import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; import { traefikMetricsSpecProvider } from './traefik_metrics'; +import { awsLogsSpecProvider } from './aws_logs'; +import { activemqMetricsSpecProvider } from './activemq_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -144,4 +146,6 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(consulMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(cockroachdbMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(traefikMetricsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); } diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 34de58048b887..9f0ad632fd5b1 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -26,6 +26,11 @@ export default function (kibana) { hidden: true, url: '/status', }, + injectDefaultVars(server) { + return { + isStatusPageAnonymous: server.config().get('status.allowAnonymous'), + }; + } } }); } diff --git a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap b/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap index ce24ff89776c8..b88210758a00d 100644 --- a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap +++ b/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap @@ -4,9 +4,7 @@ exports[`byte metric 1`] = ` `; @@ -14,9 +12,7 @@ exports[`float metric 1`] = ` `; @@ -24,9 +20,7 @@ exports[`general metric 1`] = ` `; @@ -34,8 +28,6 @@ exports[`millisecond metric 1`] = ` `; diff --git a/src/legacy/core_plugins/telemetry/README.md b/src/legacy/core_plugins/telemetry/README.md new file mode 100644 index 0000000000000..830c08f8e8bed --- /dev/null +++ b/src/legacy/core_plugins/telemetry/README.md @@ -0,0 +1,9 @@ +# Kibana Telemetry Service + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + +1. Integrating with the telemetry service to express how to collect usage data (Collecting). +2. Sending a payload of usage data up to Elastic's telemetry cluster. +3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). + +This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 5ae0d5f127eed..9f850fc0fe719 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -27,14 +27,7 @@ import { i18n } from '@kbn/i18n'; import mappings from './mappings.json'; import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server'; - -import { - createLocalizationUsageCollector, - createTelemetryUsageCollector, - createUiMetricUsageCollector, - createTelemetryPluginUsageCollector, -} from './server/collectors'; +import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; const ENDPOINT_VERSION = 'v2'; @@ -123,6 +116,7 @@ const telemetry = (kibana: any) => { fetcherTask.start(); }, init(server: Server) { + const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { packageInfo: { @@ -149,12 +143,11 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; - telemetryPlugin(initializerContext).setup(coreSetup); - // register collectors - server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server)); - server.usage.collectorSet.register(createLocalizationUsageCollector(server)); - server.usage.collectorSet.register(createTelemetryUsageCollector(server)); - server.usage.collectorSet.register(createUiMetricUsageCollector(server)); + const pluginsSetup: PluginsSetup = { + usageCollection, + }; + + telemetryPlugin(initializerContext).setup(coreSetup, pluginsSetup, server); }, }); }; diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 799d9f4ee9c8b..933c249cd7279 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -19,6 +19,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -37,6 +38,7 @@ export interface ClusterDetails { } export interface StatsCollectionConfig { + usageCollection: UsageCollectionSetup; callCluster: CallCluster; server: any; start: string; @@ -112,7 +114,8 @@ export class TelemetryCollectionManager { ? (...args: any[]) => callWithRequest(config.req, ...args) : callWithInternalUser; - return { server, callCluster, start, end }; + const { usageCollection } = server.newPlatform.setup.plugins; + return { server, callCluster, start, end, usageCollection }; }; private getOptInStatsForCollection = async ( diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts index f963ecec0477c..2f2a53278117b 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts @@ -18,7 +18,7 @@ */ export { encryptTelemetry } from './encryption'; -export { createTelemetryUsageCollector } from './usage'; -export { createUiMetricUsageCollector } from './ui_metric'; -export { createLocalizationUsageCollector } from './localization'; -export { createTelemetryPluginUsageCollector } from './telemetry_plugin'; +export { registerTelemetryUsageCollector } from './usage'; +export { registerUiMetricUsageCollector } from './ui_metric'; +export { registerLocalizationUsageCollector } from './localization'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts index 3b289752ce39f..71026b026263f 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createLocalizationUsageCollector } from './telemetry_localization_collector'; +export { registerLocalizationUsageCollector } from './telemetry_localization_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts index 74c93931096b2..191565187be14 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts @@ -21,6 +21,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { getIntegrityHashes, Integrities } from './file_integrity'; import { KIBANA_LOCALIZATION_STATS_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export interface UsageStats { locale: string; integrities: Integrities; @@ -51,15 +52,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createLocalizationUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerLocalizationUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: KIBANA_LOCALIZATION_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts index e96c47741f79c..631a37e674c4e 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts index a172ba7dc6955..5e25538cbad80 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts @@ -20,6 +20,8 @@ import { TELEMETRY_STATS_TYPE } from '../../../common/constants'; import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; + export interface TelemetryUsageStats { opt_in_status?: boolean | null; usage_fetcher?: 'browser' | 'server'; @@ -61,15 +63,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createTelemetryPluginUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerTelemetryPluginUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: TELEMETRY_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts index e1ac7a1f5af12..013db526211e1 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; +export { registerUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index fa3159669c33c..73157abce8629 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -18,10 +18,10 @@ */ import { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; -export function createUiMetricUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerUiMetricUsageCollector(usageCollection: UsageCollectionSetup, server: any) { + const collector = usageCollection.makeUsageCollector({ type: UI_METRIC_USAGE_TYPE, fetch: async () => { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; @@ -55,4 +55,6 @@ export function createUiMetricUsageCollector(server: any) { }, isReady: () => true, }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts index a1b3d5a7b1982..3ef9eed3c1265 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryUsageCollector } from './telemetry_usage_collector'; +export { registerTelemetryUsageCollector } from './telemetry_usage_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts index 3806dfc77120f..2b2e946198e0a 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts @@ -25,20 +25,15 @@ import { createTelemetryUsageCollector, isFileReadable, readTelemetryFile, - KibanaHapiServer, MAX_FILE_SIZE, } from './telemetry_usage_collector'; -const getMockServer = (): KibanaHapiServer => - ({ - usage: { - collectorSet: { makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg) }, - }, - } as KibanaHapiServer & Server); +const mockUsageCollector = () => ({ + makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg), +}); -const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { +const serverWithConfig = (configPath: string): Server => { return { - ...getMockServer(), config: () => ({ get: (key: string) => { if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') { @@ -48,7 +43,7 @@ const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { return configPath; }, }), - } as KibanaHapiServer & Server; + } as Server; }; describe('telemetry_usage_collector', () => { @@ -130,14 +125,15 @@ describe('telemetry_usage_collector', () => { }); describe('createTelemetryUsageCollector', () => { - test('calls `collectorSet.makeUsageCollector`', async () => { + test('calls `makeUsageCollector`', async () => { // note: it uses the file's path to get the directory, then looks for 'telemetry.yml' // exclusively, which is indirectly tested by passing it the wrong "file" in the same // dir - const server: KibanaHapiServer & Server = serverWithConfig(tempFiles.unreadable); + const server: Server = serverWithConfig(tempFiles.unreadable); // the `makeUsageCollector` is mocked above to return the argument passed to it - const collectorOptions = createTelemetryUsageCollector(server); + const usageCollector = mockUsageCollector() as any; + const collectorOptions = createTelemetryUsageCollector(usageCollector, server); expect(collectorOptions.type).toBe('static_telemetry'); expect(await collectorOptions.fetch()).toEqual(expectedObject); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index c927453641193..99090cb2fb7ef 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -25,20 +25,13 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** * The maximum file size before we ignore it (note: this limit is arbitrary). */ export const MAX_FILE_SIZE = 10 * 1024; // 10 KB -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: (collector: object) => any; - }; - }; -} - /** * Determine if the supplied `path` is readable. * @@ -83,19 +76,11 @@ export async function readTelemetryFile(path: string): Promise true, fetch: async () => { @@ -106,3 +91,11 @@ export function createTelemetryUsageCollector(server: KibanaHapiServer) { }, }); } + +export function registerTelemetryUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { + const collector = createTelemetryUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 02752ca773488..6c62d03adf25c 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -24,7 +24,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; export { telemetryCollectionManager } from './collection_manager'; - +export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => new TelemetryPlugin(initializerContext); export { constants }; diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index f2628090c08af..06a974f473498 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -18,8 +18,20 @@ */ import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { + registerUiMetricUsageCollector, + registerTelemetryUsageCollector, + registerLocalizationUsageCollector, + registerTelemetryPluginUsageCollector, +} from './collectors'; + +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; +} export class TelemetryPlugin { private readonly currentKibanaVersion: string; @@ -28,9 +40,15 @@ export class TelemetryPlugin { this.currentKibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup, server: Server) { const currentKibanaVersion = this.currentKibanaVersion; + registerCollection(); registerRoutes({ core, currentKibanaVersion }); + + registerTelemetryPluginUsageCollector(usageCollection, server); + registerLocalizationUsageCollector(usageCollection, server); + registerTelemetryUsageCollector(usageCollection, server); + registerUiMetricUsageCollector(usageCollection, server); } } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index 4cbdf18df4a74..140204ac5ab49 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -29,7 +29,12 @@ import { handleLocalStats, } from '../get_local_stats'; -const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ +const mockUsageCollection = (kibanaUsage = {}) => ({ + bulkFetch: () => kibanaUsage, + toObject: data => data, +}); + +const getMockServer = (getCluster = sinon.stub()) => ({ log(tags, message) { console.log({ tags, message }); }, @@ -43,7 +48,6 @@ const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ } }; }, - usage: { collectorSet: { bulkFetch: () => kibanaUsage, toObject: data => data } }, plugins: { elasticsearch: { getCluster }, }, @@ -155,15 +159,16 @@ describe('get_local_stats', () => { describe.skip('getLocalStats', () => { it('returns expected object without xpack data when X-Pack fails to respond', async () => { const callClusterUsageFailed = sinon.stub(); - + const usageCollection = mockUsageCollection(); mockGetLocalStats( callClusterUsageFailed, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ + const result = await getLocalStats([], { server: getMockServer(), callCluster: callClusterUsageFailed, + usageCollection, }); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); @@ -178,15 +183,16 @@ describe('get_local_stats', () => { it('returns expected object with xpack and kibana data', async () => { const callCluster = sinon.stub(); - + const usageCollection = mockUsageCollection(kibana); mockGetLocalStats( callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ - server: getMockServer(callCluster, kibana), + const result = await getLocalStats([], { + server: getMockServer(callCluster), + usageCollection, callCluster, }); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js index 051ef370fcde5..236dd046148f6 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js @@ -47,12 +47,7 @@ export function handleKibanaStats(server, response) { }; } -/* - * Check user privileges for read access to monitoring - * Pass callWithInternalUser to bulkFetchUsage - */ -export async function getKibana(server, callWithInternalUser) { - const { collectorSet } = server.usage; - const usage = await collectorSet.bulkFetch(callWithInternalUser); - return collectorSet.toObject(usage); +export async function getKibana(usageCollection, callWithInternalUser) { + const usage = await usageCollection.bulkFetch(callWithInternalUser); + return usageCollection.toObject(usage); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index e11c6b1277d5b..a4ea2eb534226 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -55,13 +55,14 @@ export function handleLocalStats(server: any, clusterInfo: any, clusterStats: an * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ export const getLocalStats: StatsGetter = async (clustersDetails, config) => { - const { server, callCluster } = config; + const { server, callCluster, usageCollection } = config; + return await Promise.all( clustersDetails.map(async clustersDetail => { const [clusterInfo, clusterStats, kibana] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) - getKibana(server, callCluster), + getKibana(usageCollection, callCluster), ]); return handleLocalStats(server, clusterInfo, clusterStats, kibana); }) diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.test.ts index a6ca444de6d4c..af142973a535d 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.test.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.test.ts @@ -25,7 +25,7 @@ describe('getNotifyUserAboutOptInDefault: get a flag that describes if the user getNotifyUserAboutOptInDefault({ allowChangingOptInStatus: true, telemetrySavedObject: { userHasSeenNotice: false }, - telemetryOptedIn: null, + telemetryOptedIn: true, configTelemetryOptIn: true, }) ).toBe(true); @@ -40,50 +40,37 @@ describe('getNotifyUserAboutOptInDefault: get a flag that describes if the user configTelemetryOptIn: false, }) ).toBe(false); - }); - it('should return false if user has seen notice', () => { expect( getNotifyUserAboutOptInDefault({ - allowChangingOptInStatus: true, - telemetrySavedObject: { userHasSeenNotice: true }, - telemetryOptedIn: false, + allowChangingOptInStatus: false, + telemetrySavedObject: null, + telemetryOptedIn: true, configTelemetryOptIn: true, }) ).toBe(false); + }); + it('should return false if user has seen notice', () => { expect( getNotifyUserAboutOptInDefault({ allowChangingOptInStatus: true, telemetrySavedObject: { userHasSeenNotice: true }, - telemetryOptedIn: true, - configTelemetryOptIn: true, + telemetryOptedIn: false, + configTelemetryOptIn: false, }) ).toBe(false); - }); - it('not show notice for users already opted in and has not seen notice yet', () => { expect( getNotifyUserAboutOptInDefault({ allowChangingOptInStatus: true, - telemetrySavedObject: { userHasSeenNotice: false }, + telemetrySavedObject: { userHasSeenNotice: true }, telemetryOptedIn: true, configTelemetryOptIn: true, }) ).toBe(false); }); - it('should see notice if they are merely opted in by default and have not yet seen the notice', () => { - expect( - getNotifyUserAboutOptInDefault({ - allowChangingOptInStatus: true, - telemetrySavedObject: { userHasSeenNotice: false }, - telemetryOptedIn: null, - configTelemetryOptIn: true, - }) - ).toBe(true); - }); - it('should return false if user is opted out', () => { expect( getNotifyUserAboutOptInDefault({ diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.ts index eb95aff6392e0..8ef3bd8388ecb 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_notify_user_about_optin_default.ts @@ -41,9 +41,5 @@ export function getNotifyUserAboutOptInDefault({ return false; } - if (telemetryOptedIn !== null) { - return false; // they were not defaulted in - } - - return configTelemetryOptIn; + return telemetryOptedIn === true && configTelemetryOptIn === true; } diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index 5bc5355d7c061..04edfc5b61141 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -30,8 +30,6 @@ import { timefilter } from 'ui/timefilter'; import { npStart } from 'ui/new_platform'; import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; -// import the uiExports that we want to "use" -import 'uiExports/fieldFormats'; import 'uiExports/savedObjectTypes'; require('ui/autoload/all'); diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts index 74111bf794877..14cd3d0083e6a 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts @@ -83,7 +83,7 @@ export function getTimelionRequestHandler(dependencies: TimelionVisualizationDep sheet: [expression], extended: { es: { - filter: esQuery.buildEsQuery(null, query, filters, esQueryConfigs), + filter: esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs), }, }, time: { diff --git a/src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap b/src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap index 9aee51aa6c93d..d0fba4d164dbf 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap +++ b/src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "metric": Object { "colorSchema": "\\"Green to Red\\"", - "colorsRange": undefined, + "colorsRange": "{range from=0 to=10000}", "invertColors": false, "labels": Object { "show": true, diff --git a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis_controller.js b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis_controller.js index 8dd2b093c6f91..9f58d00d38271 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis_controller.js +++ b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis_controller.js @@ -51,7 +51,7 @@ describe('metric_vis - controller', function () { expect(metrics.length).to.be(1); expect(metrics[0].label).to.be('Count'); - expect(metrics[0].value).to.be(4301021); + expect(metrics[0].value).to.be('4301021'); }); it('should support multi-value metrics', function () { @@ -66,8 +66,8 @@ describe('metric_vis - controller', function () { expect(metrics.length).to.be(2); expect(metrics[0].label).to.be('1st percentile of bytes'); - expect(metrics[0].value).to.be(182); + expect(metrics[0].value).to.be('182'); expect(metrics[1].label).to.be('99th percentile of bytes'); - expect(metrics[1].value).to.be(445842.4634666484); + expect(metrics[1].value).to.be('445842.4634666484'); }); }); diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts index 39bc96933c66e..b64361f17c470 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts @@ -99,6 +99,7 @@ export const createMetricVisFn = (): ExpressionFunction< colorRange: { types: ['range'], multi: true, + default: '{range from=0 to=10000}', help: i18n.translate('visTypeMetric.function.colorRange.help', { defaultMessage: 'A range object specifying groups of values to which different colors should be applied.', diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js index 0459d11c74ef0..1192b678ae148 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js @@ -19,10 +19,14 @@ import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; -import { fieldFormats } from 'ui/registry/field_formats'; +import { npStart } from 'ui/new_platform'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; + + export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => { + const fieldFormats = npStart.plugins.data.fieldFormats; + if (!template) template = '{{value}}'; const render = handlebars.compile(template, { knownHelpersOnly: true }); let formatter; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 2b42c22ad7c43..1d42b77336933 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { VisEditorVisualization } from './vis_editor_visualization'; import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; @@ -30,6 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../common/extract_index_patterns'; +import { esKuery } from '../../../../../plugins/data/public'; import { npStart } from 'ui/new_platform'; @@ -88,7 +88,7 @@ export class VisEditor extends Component { if (filterQuery && filterQuery.language === 'kuery') { try { const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); - fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); + esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js index 8af175d116556..10fc34fccd2cc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js @@ -20,18 +20,17 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { fieldFormats } from 'ui/registry/field_formats'; +import { npStart } from 'ui/new_platform'; import { createTickFormatter } from '../../lib/tick_formatter'; import { calculateLabel } from '../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; +import { FIELD_FORMAT_IDS } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPES } from '../../../../common/metric_types'; -const DateFormat = fieldFormats.getType('date'); - function getColor(rules, colorKey, value) { let color; if (rules) { @@ -49,6 +48,10 @@ function getColor(rules, colorKey, value) { export class TableVis extends Component { constructor(props) { super(props); + + const fieldFormats = npStart.plugins.data.fieldFormats; + const DateFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE); + this.dateFormatter = new DateFormat({}, this.props.getConfig); } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index 83ae31bf87400..26380bf2b9d94 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -49,7 +49,7 @@ export function createVegaRequestHandler({ timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - const filtersDsl = esQuery.buildEsQuery(null, query, filters, esQueryConfigs); + const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); return vp.parseAsync(); diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index 97d3a4352c2f8..d987260b099bd 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -62,6 +62,8 @@ export interface LegacyPluginOptions { }>; apps: any; hacks: string[]; + visualize: string[]; + devTools: string[]; styleSheetPaths: string; injectDefaultVars: (server: Server) => Record; noParse: string[]; diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index db5c0e9665492..0915f7de25b45 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -38,6 +38,7 @@ describe('config/deprecation warnings', function () { ], { stdio: ['ignore', 'pipe', 'pipe'], env: { + ...process.env, CREATE_SERVER_OPTS: JSON.stringify({ logging: { quiet: false, @@ -50,9 +51,9 @@ describe('config/deprecation warnings', function () { } }); - // Either time out in 10 seconds, or resolve once the line is in our buffer + // Either time out in 60 seconds, or resolve once the line is in our buffer return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 10000)), + new Promise((resolve) => setTimeout(resolve, 60000)), new Promise((resolve, reject) => { proc.stdout.on('data', (chunk) => { stdio += chunk.toString('utf8'); @@ -105,7 +106,11 @@ describe('config/deprecation warnings', function () { line.tags.includes('warning') ); - expect(deprecationLines).to.have.length(1); - expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); + try { + expect(deprecationLines).to.have.length(1); + expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); + } catch (error) { + throw new Error(`Expected stdio to include deprecation message about uiSettings.enabled\n\nstdio:\n${stdio}\n\n`); + } }); }); diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js index 7cac17a88fe64..b23a1de2c0773 100644 --- a/src/legacy/server/config/transform_deprecations.js +++ b/src/legacy/server/config/transform_deprecations.js @@ -102,6 +102,10 @@ const deprecations = [ rename('optimize.lazyHost', 'optimize.watchHost'), rename('optimize.lazyPrebuild', 'optimize.watchPrebuild'), rename('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + rename('xpack.telemetry.enabled', 'telemetry.enabled'), + rename('xpack.telemetry.config', 'telemetry.config'), + rename('xpack.telemetry.banner', 'telemetry.banner'), + rename('xpack.telemetry.url', 'telemetry.url'), savedObjectsIndexCheckTimeout, rewriteBasePath, configPath, diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 9cc4e30d4252d..6f2730476956e 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -38,7 +38,7 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/serve import { SavedObjectsManagement } from '../../core/server/saved_objects/management'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; - +import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/public'; @@ -67,7 +67,6 @@ declare module 'hapi' { config: () => KibanaConfig; indexPatternsServiceFactory: IndexPatternsServiceFactory; savedObjects: SavedObjectsLegacyService; - usage: { collectorSet: any }; injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; getHiddenUiAppById(appId: string): UiApp; registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void; @@ -101,6 +100,11 @@ declare module 'hapi' { type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; + [key: string]: object; +} + // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: { @@ -120,7 +124,7 @@ export default class KbnServer { }; setup: { core: CoreSetup; - plugins: Record; + plugins: PluginsSetup; }; start: { core: CoreSetup; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index f7ed56b10c267..e5f182c931d80 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -28,7 +28,6 @@ import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; -import { usageMixin } from './usage'; import { statusMixin } from './status'; import pidMixin from './pid'; import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; @@ -94,7 +93,6 @@ export default class KbnServer { loggingMixin, configDeprecationWarningsMixin, warningsMixin, - usageMixin, statusMixin, // writes pid file diff --git a/src/legacy/server/sample_data/usage/collector.ts b/src/legacy/server/sample_data/usage/collector.ts index 8561a6c3f1007..bcb5e7be2597a 100644 --- a/src/legacy/server/sample_data/usage/collector.ts +++ b/src/legacy/server/sample_data/usage/collector.ts @@ -17,26 +17,25 @@ * under the License. */ -import * as Hapi from 'hapi'; +import { Server } from 'hapi'; import { fetchProvider } from './collector_fetch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; -interface KbnServer extends Hapi.Server { - usage: any; -} - -export function makeSampleDataUsageCollector(server: KbnServer) { +export function makeSampleDataUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { let index: string; try { index = server.config().get('kibana.index'); } catch (err) { return; // kibana plugin is not enabled (test environment) } + const collector = usageCollection.makeUsageCollector({ + type: 'sample-data', + fetch: fetchProvider(index), + isReady: () => true, + }); - server.usage.collectorSet.register( - server.usage.collectorSet.makeUsageCollector({ - type: 'sample-data', - fetch: fetchProvider(index), - isReady: () => true, - }) - ); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js index aded85384fd85..116e588c5ade6 100644 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ b/src/legacy/server/status/collectors/get_ops_stats_collector.js @@ -35,9 +35,8 @@ import { getKibanaInfoForStats } from '../lib'; * the metrics. * See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647 */ -export function getOpsStatsCollector(server, kbnServer) { - const { collectorSet } = server.usage; - return collectorSet.makeStatsCollector({ +export function getOpsStatsCollector(usageCollection, server, kbnServer) { + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE, fetch: () => { return { @@ -49,3 +48,10 @@ export function getOpsStatsCollector(server, kbnServer) { ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. }); } + +export function registerOpsStatsCollector(usageCollection, server, kbnServer) { + if (usageCollection) { + const collector = getOpsStatsCollector(usageCollection, server, kbnServer); + usageCollection.registerCollector(collector); + } +} diff --git a/src/legacy/server/status/collectors/index.js b/src/legacy/server/status/collectors/index.js index 4310dff7359ef..92d9e601bbb35 100644 --- a/src/legacy/server/status/collectors/index.js +++ b/src/legacy/server/status/collectors/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { getOpsStatsCollector } from './get_ops_stats_collector'; +export { registerOpsStatsCollector } from './get_ops_stats_collector'; diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js index dda20878605e5..ba2f835599bc9 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -20,17 +20,15 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; -import { getOpsStatsCollector } from './collectors'; +import { registerOpsStatsCollector } from './collectors'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; export function statusMixin(kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); - - const statsCollector = getOpsStatsCollector(server, kbnServer); - const { collectorSet } = server.usage; - collectorSet.register(statsCollector); + const { usageCollection } = server.newPlatform.setup.plugins; + registerOpsStatsCollector(usageCollection, server, kbnServer); const metrics = new Metrics(config, server); @@ -57,7 +55,7 @@ export function statusMixin(kbnServer, server, config) { // init routes registerStatusPage(kbnServer, server, config); registerStatusApi(kbnServer, server, config); - registerStatsApi(kbnServer, server, config); + registerStatsApi(usageCollection, server, config); // expore shared functionality server.decorate('server', 'getOSInfo', getOSInfo); diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 91272ead1d2c1..366d36860731c 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -29,7 +29,7 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { /* * API for Kibana meta info and accumulated operations stats - * Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data + * Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data * - Requests to set isExtended = true * GET /api/stats?extended=true * GET /api/stats?extended @@ -37,9 +37,8 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { * - Any other value causes a statusCode 400 response (Bad Request) * Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended */ -export function registerStatsApi(kbnServer, server, config) { +export function registerStatsApi(usageCollection, server, config) { const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - const { collectorSet } = server.usage; const getClusterUuid = async callCluster => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid', }); @@ -47,8 +46,8 @@ export function registerStatsApi(kbnServer, server, config) { }; const getUsage = async callCluster => { - const usage = await collectorSet.bulkFetchUsage(callCluster); - return collectorSet.toObject(usage); + const usage = await usageCollection.bulkFetchUsage(callCluster); + return usageCollection.toObject(usage); }; server.route( @@ -74,7 +73,7 @@ export function registerStatsApi(kbnServer, server, config) { if (isExtended) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const callCluster = (...args) => callWithRequest(req, ...args); - const collectorsReady = await collectorSet.areAllCollectorsReady(); + const collectorsReady = await usageCollection.areAllCollectorsReady(); if (shouldGetUsage && !collectorsReady) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); @@ -126,7 +125,7 @@ export function registerStatsApi(kbnServer, server, config) { }; } else { - extended = collectorSet.toApiFieldNames({ + extended = usageCollection.toApiFieldNames({ usage: modifiedUsage, clusterUuid }); @@ -139,12 +138,12 @@ export function registerStatsApi(kbnServer, server, config) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); } let kibanaStats = await kibanaStatsCollector.fetch(); - kibanaStats = collectorSet.toApiFieldNames(kibanaStats); + kibanaStats = usageCollection.toApiFieldNames(kibanaStats); return { ...kibanaStats, diff --git a/src/legacy/server/usage/README.md b/src/legacy/server/usage/README.md deleted file mode 100644 index 5c4bcc05bbc38..0000000000000 --- a/src/legacy/server/usage/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Kibana Telemetry Service - -Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: - -1. Integrating with the telemetry service to express how to collect usage data (Collecting). -2. Sending a payload of usage data up to Elastic's telemetry cluster, once per browser per day (Sending). -3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). - -You, the feature or plugin developer, mainly need to worry about the first meaning: collecting. To integrate with the telemetry services for usage collection of your feature, there are 2 steps: - -1. Create a usage collector using a factory function -2. Register the usage collector with the Telemetry service - -NOTE: To a lesser extent, there's also a need to update the telemetry payload of Kibana stats and telemetry cluster field mappings to include your fields. This part is typically handled not by you, the developer, but different maintainers of the telemetry cluster. Usually, this step just means talk to the Platform team and have them approve your data model or added fields. - -## Creating and Registering Usage Collector - -A usage collector object is an instance of a class called `UsageCollector`. A factory function on `server.usage.collectorSet` object allows you to create an instance of this class. All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. - -Example: - -```js -// create usage collector -const myCollector = server.usage.collectorSet.makeUsageCollector({ - type: MY_USAGE_TYPE, - fetch: async callCluster => { - - // query ES and get some data - // summarize the data into a model - // return the modeled object that includes whatever you want to track - - return { - my_objects: { - total: SOME_NUMBER - } - }; - }, -}); - -// register usage collector -server.usage.collectorSet.register(myCollector); -``` - -Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. - -The fetch method also might be called through an internal background task on the Kibana server, which currently lives in the `kibana_monitoring` module of the X-Pack Monitoring plugin, that polls for data and uploads it to Elasticsearch through a bulk API exposed by the Monitoring plugin for Elasticsearch. In this case, the `callCluster` method will be the internal system user and will have read privilege over the entire `.kibana` index. - -Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. - - -Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. - -## Update the telemetry payload and telemetry cluster field mappings - -There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. - -As of the time of this writing (pre-6.5.0) there are a few unpleasant realities with this module. Today, this module has to be aware of all the features that have integrated with it, which it does from hard-coding. It does this because at the time of creation, the payload implemented a designed model where X-Pack plugin info went together regardless if it was ES-specific or Kibana-specific. In hindsight, all the Kibana data could just be put together, X-Pack or not, which it could do in a generic way. This is a known problem and a solution will be implemented in an upcoming refactoring phase, as this would break the contract for model of data sent in the payload. - -The second reality is that new fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. - -## Testing - -There are a few ways you can test that your usage collector is working properly. - -1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. -2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. ✳ - - The dev script in x-pack can be run on the command-line with: - ``` - cd x-pack - node scripts/api_debug.js telemetry --host=http://localhost:5601 - ``` - Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. - - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 -3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. -4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. - -✳ At the time of this writing, there is an open issue that in the sending phase, Kibana usage collectors are not "live-pulled" from Kibana API endpoints if Monitoring is disabled. The implementation on this depends on a new secure way to live-pull the data from the end-user's browser, as it would not be appropriate to supply only partial data if the logged-in user only has partial access to `.kibana`. - -## FAQ - -1. **Can telemetry track UI interactions, such as button click?** - Brief answer: no. Telemetry collection happens on the server-side so the usage data will only include information that the server-side is aware of. There is no generic way to do this today, but UI-interaction KPIs can be tracked with a custom server endpoint that gets called for tracking when the UI event happens. -2. **Does the telemetry service have a hook that I can call whenever some event happens in my feature?** - Brief answer: no. Telemetry collection is a fetch model, not a push model. Telemetry fetches info from your collector. -3. **How should I design my data model?** - Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. -4. **Can the telemetry payload include dynamic fields?** - Yes. When you talk to the Platform team about new fields being added, point out specifically which properties will have dynamic inner fields. -5. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** - Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/legacy/server/usage/classes/__tests__/collector_set.js b/src/legacy/server/usage/classes/__tests__/collector_set.js deleted file mode 100644 index 5cf18a8a15200..0000000000000 --- a/src/legacy/server/usage/classes/__tests__/collector_set.js +++ /dev/null @@ -1,184 +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 { noop } from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { Collector } from '../collector'; -import { CollectorSet } from '../collector_set'; -import { UsageCollector } from '../usage_collector'; - -describe('CollectorSet', () => { - describe('registers a collector set and runs lifecycle events', () => { - let server; - let init; - let fetch; - - beforeEach(() => { - server = { log: sinon.spy() }; - init = noop; - fetch = noop; - }); - - it('should throw an error if non-Collector type of object is registered', () => { - const collectors = new CollectorSet(server); - const registerPojo = () => { - collectors.register({ - type: 'type_collector_test', - init, - fetch, - }); - }; - - expect(registerPojo).to.throwException(({ message }) => { - expect(message).to.be('CollectorSet can only have Collector instances registered'); - }); - }); - - it('should log debug status of fetching from the collector', async () => { - const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { - type: 'MY_TEST_COLLECTOR', - fetch: caller => caller() - })); - - const result = await collectors.bulkFetch(mockCallCluster); - const calls = server.log.getCalls(); - expect(calls.length).to.be(1); - expect(calls[0].args).to.eql([ - ['debug', 'stats-collection'], - 'Fetching data from MY_TEST_COLLECTOR collector', - ]); - expect(result).to.eql([{ - type: 'MY_TEST_COLLECTOR', - result: { passTest: 1000 } - }]); - }); - - it('should gracefully handle a collector fetch method throwing an error', async () => { - const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { - type: 'MY_TEST_COLLECTOR', - fetch: () => new Promise((_resolve, reject) => reject()) - })); - - let result; - try { - result = await collectors.bulkFetch(mockCallCluster); - } catch (err) { - // Do nothing - } - // This must return an empty object instead of null/undefined - expect(result).to.eql([]); - }); - }); - - describe('toApiFieldNames', () => { - let collectorSet; - - beforeEach(() => { - collectorSet = new CollectorSet(); - }); - - it('should snake_case and convert field names to api standards', () => { - const apiData = { - os: { - load: { - '15m': 2.3525390625, - '1m': 2.22412109375, - '5m': 2.4462890625 - }, - memory: { - free_in_bytes: 458280960, - total_in_bytes: 17179869184, - used_in_bytes: 16721588224 - }, - uptime_in_millis: 137844000 - }, - daysOfTheWeek: [ - 'monday', - 'tuesday', - 'wednesday', - ] - }; - - const result = collectorSet.toApiFieldNames(apiData); - expect(result).to.eql({ - os: { - load: { '15m': 2.3525390625, '1m': 2.22412109375, '5m': 2.4462890625 }, - memory: { free_bytes: 458280960, total_bytes: 17179869184, used_bytes: 16721588224 }, - uptime_ms: 137844000, - }, - days_of_the_week: ['monday', 'tuesday', 'wednesday'], - }); - }); - - it('should correct object key fields nested in arrays', () => { - const apiData = { - daysOfTheWeek: [ - { - dayName: 'monday', - dayIndex: 1 - }, - { - dayName: 'tuesday', - dayIndex: 2 - }, - { - dayName: 'wednesday', - dayIndex: 3 - } - ] - }; - - const result = collectorSet.toApiFieldNames(apiData); - expect(result).to.eql({ - days_of_the_week: [ - { day_index: 1, day_name: 'monday' }, - { day_index: 2, day_name: 'tuesday' }, - { day_index: 3, day_name: 'wednesday' }, - ], - }); - }); - }); - - describe('isUsageCollector', () => { - const server = { }; - const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} }; - - it('returns true only for UsageCollector instances', () => { - const collectors = new CollectorSet(server); - - const usageCollector = new UsageCollector(server, collectorOptions); - const collector = new Collector(server, collectorOptions); - const randomClass = new (class Random {}); - expect(collectors.isUsageCollector(usageCollector)).to.be(true); - expect(collectors.isUsageCollector(collector)).to.be(false); - expect(collectors.isUsageCollector(randomClass)).to.be(false); - expect(collectors.isUsageCollector({})).to.be(false); - expect(collectors.isUsageCollector(null)).to.be(false); - expect(collectors.isUsageCollector('')).to.be(false); - expect(collectors.isUsageCollector()).to.be(false); - }); - }); -}); - - diff --git a/src/legacy/server/usage/classes/collector_set.js b/src/legacy/server/usage/classes/collector_set.js deleted file mode 100644 index 5a86992f0af71..0000000000000 --- a/src/legacy/server/usage/classes/collector_set.js +++ /dev/null @@ -1,206 +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 { snakeCase } from 'lodash'; -import { getCollectorLogger } from '../lib'; -import { Collector } from './collector'; -import { UsageCollector } from './usage_collector'; - -let _waitingForAllCollectorsTimestamp = null; - -/* - * A collector object has types registered into it with the register(type) - * function. Each type that gets registered defines how to fetch its own data - * and optionally, how to combine it into a unified payload for bulk upload. - */ -export class CollectorSet { - /* - * @param {Object} server - server object - * @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance - */ - constructor(server, collectors = [], config = null) { - this._log = getCollectorLogger(server); - this._collectors = collectors; - - /* - * Helper Factory methods - * Define as instance properties to allow enclosing the server object - */ - this.makeStatsCollector = options => new Collector(server, options); - this.makeUsageCollector = options => new UsageCollector(server, options); - this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config); - - this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60; - } - - /* - * @param collector {Collector} collector object - */ - register(collector) { - // check instanceof - if (!(collector instanceof Collector)) { - throw new Error('CollectorSet can only have Collector instances registered'); - } - - this._collectors.push(collector); - - if (collector.init) { - this._log.debug(`Initializing ${collector.type} collector`); - collector.init(); - } - } - - getCollectorByType(type) { - return this._collectors.find(c => c.type === type); - } - - // isUsageCollector(x: UsageCollector | any): x is UsageCollector { - isUsageCollector(x) { - return x instanceof UsageCollector; - } - - async areAllCollectorsReady(collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const collectorTypesNotReady = []; - let allReady = true; - await collectorSet.asyncEach(async collector => { - if (!await collector.isReady()) { - allReady = false; - collectorTypesNotReady.push(collector.type); - } - }); - - if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) { - const nowTimestamp = +new Date(); - _waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp; - const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp; - const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS; - if (timeLeftInMS <= 0) { - this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` - + `but we have waited the required ` - + `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`); - return true; - } else { - this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); - } - } else { - _waitingForAllCollectorsTimestamp = null; - } - - return allReady; - } - - /* - * Call a bunch of fetch methods and then do them in bulk - * @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors - */ - async bulkFetch(callCluster, collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`bulkFetch method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const responses = []; - await collectorSet.asyncEach(async collector => { - this._log.debug(`Fetching data from ${collector.type} collector`); - try { - responses.push({ - type: collector.type, - result: await collector.fetchInternal(callCluster) - }); - } - catch (err) { - this._log.warn(err); - this._log.warn(`Unable to fetch data from ${collector.type} collector`); - } - }); - return responses; - } - - /* - * @return {new CollectorSet} - */ - getFilteredCollectorSet(filter) { - const filtered = this._collectors.filter(filter); - return this._makeCollectorSetFromArray(filtered); - } - - async bulkFetchUsage(callCluster) { - const usageCollectors = this.getFilteredCollectorSet(c => c instanceof UsageCollector); - return this.bulkFetch(callCluster, usageCollectors); - } - - // convert an array of fetched stats results into key/object - toObject(statsData) { - if (!statsData) return {}; - return statsData.reduce((accumulatedStats, { type, result }) => { - return { - ...accumulatedStats, - [type]: result, - }; - }, {}); - } - - // rename fields to use api conventions - toApiFieldNames(apiData) { - const getValueOrRecurse = value => { - if (value == null || typeof value !== 'object') { - return value; - } else { - return this.toApiFieldNames(value); // recurse - } - }; - - // handle array and return early, or return a reduced object - - if (Array.isArray(apiData)) { - return apiData.map(getValueOrRecurse); - } - - return Object.keys(apiData).reduce((accum, field) => { - const value = apiData[field]; - let newName = field; - newName = snakeCase(newName); - newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m - newName = newName.replace('_in_bytes', '_bytes'); - newName = newName.replace('_in_millis', '_ms'); - - return { - ...accum, - [newName]: getValueOrRecurse(value), - }; - }, {}); - } - - map(mapFn) { - return this._collectors.map(mapFn); - } - - some(someFn) { - return this._collectors.some(someFn); - } - - async asyncEach(eachFn) { - for (const collector of this._collectors) { - await eachFn(collector); - } - } -} diff --git a/src/legacy/server/usage/classes/index.js b/src/legacy/server/usage/classes/index.js deleted file mode 100644 index 0d3939e1dc681..0000000000000 --- a/src/legacy/server/usage/classes/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { CollectorSet } from './collector_set'; -export { Collector } from './collector'; -export { UsageCollector } from './usage_collector'; diff --git a/src/legacy/server/usage/index.js b/src/legacy/server/usage/index.js deleted file mode 100644 index 2a02070a55f95..0000000000000 --- a/src/legacy/server/usage/index.js +++ /dev/null @@ -1,31 +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 { CollectorSet } from './classes'; - -export function usageMixin(kbnServer, server, config) { - const collectorSet = new CollectorSet(server, undefined, config); - - /* - * expose the collector set object on the server - * provides factory methods for feature owners to create their own collector objects - * use collectorSet.register(collector) to register your feature's collector object(s) - */ - server.decorate('server', 'usage', { collectorSet }); -} diff --git a/src/legacy/server/usage/lib/get_collector_logger.js b/src/legacy/server/usage/lib/get_collector_logger.js deleted file mode 100644 index 023bf6bf635a8..0000000000000 --- a/src/legacy/server/usage/lib/get_collector_logger.js +++ /dev/null @@ -1,31 +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. - */ - -const LOGGING_TAGS = ['stats-collection']; -/* - * @param {Object} server - * @return {Object} helpful logger object - */ -export function getCollectorLogger(server) { - return { - debug: message => server.log(['debug', ...LOGGING_TAGS], message), - info: message => server.log(['info', ...LOGGING_TAGS], message), - warn: message => server.log(['warning', ...LOGGING_TAGS], message) - }; -} diff --git a/src/legacy/server/usage/lib/index.js b/src/legacy/server/usage/lib/index.js deleted file mode 100644 index 7db3cd4506503..0000000000000 --- a/src/legacy/server/usage/lib/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { getCollectorLogger } from './get_collector_logger'; diff --git a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts index 66466b96abe83..370c312706242 100644 --- a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts +++ b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts @@ -20,10 +20,10 @@ import { has } from 'lodash'; import { Legacy } from 'kibana'; import { FieldFormatsService } from './field_formats_service'; -import { FieldFormat } from '../../../../plugins/data/public'; +import { IFieldFormatType } from '../../../../plugins/data/public'; export function fieldFormatsMixin(kbnServer: any, server: Legacy.Server) { - const fieldFormatClasses: Array = []; + const fieldFormatClasses: IFieldFormatType[] = []; // for use outside of the request context, for special cases server.decorate('server', 'fieldFormatServiceFactory', async function(uiSettings) { diff --git a/src/legacy/ui/field_formats/mixin/field_formats_service.ts b/src/legacy/ui/field_formats/mixin/field_formats_service.ts index c0800fcd4162b..c5bc25333985b 100644 --- a/src/legacy/ui/field_formats/mixin/field_formats_service.ts +++ b/src/legacy/ui/field_formats/mixin/field_formats_service.ts @@ -18,7 +18,7 @@ */ import { indexBy, Dictionary } from 'lodash'; -import { FieldFormat } from '../../../../plugins/data/public'; +import { FieldFormat, IFieldFormatType } from '../../../../plugins/data/common'; interface FieldFormatConfig { id: string; @@ -27,9 +27,9 @@ interface FieldFormatConfig { export class FieldFormatsService { getConfig: any; - _fieldFormats: Dictionary; + _fieldFormats: Dictionary; - constructor(fieldFormatClasses: Array, getConfig: Function) { + constructor(fieldFormatClasses: IFieldFormatType[], getConfig: Function) { this._fieldFormats = indexBy(fieldFormatClasses, 'id'); this.getConfig = getConfig; } diff --git a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js b/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js index ce7d87c228fbd..49d814c33209c 100644 --- a/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js +++ b/src/legacy/ui/public/agg_response/hierarchical/build_hierarchical_data.test.js @@ -20,16 +20,7 @@ import { buildHierarchicalData } from './build_hierarchical_data'; import { legacyResponseHandlerProvider } from '../../vis/response_handlers/legacy'; -jest.mock('../../registry/field_formats', () => { - return { fieldFormats: { - getType: id => { - if(id === '1') { return jest.fn(); } - if(id === 'agg_1') { return jest.fn(); } - } - } - }; -} -); +jest.mock('ui/new_platform'); jest.mock('../../chrome', () => ({ getUiSettingsClient: jest.fn() diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index becfaf8c89e27..d4ef203721456 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -26,16 +26,15 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; import { AggType } from './agg_type'; import { FieldParamType } from './param_types/field'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { writeParams } from './agg_params'; import { AggConfigs } from './agg_configs'; import { Schema } from '../vis/editors/default/schemas'; -import { ContentType } from '../../../../plugins/data/public'; - -// @ts-ignore -import { fieldFormats } from '../registry/field_formats'; +import { ContentType, KBN_FIELD_TYPES } from '../../../../plugins/data/public'; export interface AggConfigOptions { enabled: boolean; @@ -235,10 +234,10 @@ export class AggConfig { /** * Hook for pre-flight logic, see AggType#onSearchRequestStart * @param {Courier.SearchSource} searchSource - * @param {Courier.SearchRequest} searchRequest + * @param {Courier.FetchOptions} options * @return {Promise} */ - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { if (!this.type) { return Promise.resolve(); } @@ -378,14 +377,16 @@ export class AggConfig { if (format) { return format.getConverterFor(contentType); } + return this.fieldOwnFormatter(contentType, defaultFormat); } fieldOwnFormatter(contentType?: ContentType, defaultFormat?: any) { + const fieldFormats = npStart.plugins.data.fieldFormats; const field = this.getField(); let format = field && field.format; if (!format) format = defaultFormat; - if (!format) format = fieldFormats.getDefaultInstance('string'); + if (!format) format = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING); return format.getConverterFor(contentType); } diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 7c0245f30a1fd..2f6951891f84d 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -33,6 +33,7 @@ import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { IndexPattern } from '../../../core_plugins/data/public'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; function removeParentAggs(obj: any) { for (const prop in obj) { @@ -301,7 +302,7 @@ export class AggConfigs { return _.find(reqAgg.getResponseAggs(), { id }); } - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { return Promise.all( // @ts-ignore this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) diff --git a/src/legacy/ui/public/agg_types/agg_type.test.ts b/src/legacy/ui/public/agg_types/agg_type.test.ts index 1c1453b74fe98..9b34910e81e88 100644 --- a/src/legacy/ui/public/agg_types/agg_type.test.ts +++ b/src/legacy/ui/public/agg_types/agg_type.test.ts @@ -19,15 +19,10 @@ import { AggType, AggTypeConfig } from './agg_type'; import { AggConfig } from './agg_config'; +import { npStart } from 'ui/new_platform'; jest.mock('ui/new_platform'); -jest.mock('ui/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn(() => 'default'), - }, -})); - describe('AggType Class', () => { describe('constructor', () => { it("requires a valid config object as it's first param", () => { @@ -158,6 +153,8 @@ describe('AggType Class', () => { }); it('returns default formatter', () => { + npStart.plugins.data.fieldFormats.getDefaultInstance = jest.fn(() => 'default') as any; + const aggType = new AggType({ name: 'name', title: 'title', diff --git a/src/legacy/ui/public/agg_types/agg_type.ts b/src/legacy/ui/public/agg_types/agg_type.ts index 7be8ec1406d3c..5216affb3e52d 100644 --- a/src/legacy/ui/public/agg_types/agg_type.ts +++ b/src/legacy/ui/public/agg_types/agg_type.ts @@ -19,6 +19,7 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { AggParam, initParams } from './agg_params'; import { AggConfig } from '../vis'; @@ -27,8 +28,7 @@ import { SearchSource } from '../courier'; import { Adapters } from '../inspector'; import { BaseParamType } from './param_types/base'; -// @ts-ignore -import { FieldFormat, fieldFormats } from '../registry/field_formats'; +import { KBN_FIELD_TYPES, FieldFormat } from '../../../../plugins/data/public'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, @@ -62,7 +62,9 @@ export interface AggTypeConfig< const getFormat = (agg: AggConfig) => { const field = agg.getField(); - return field ? field.format : fieldFormats.getDefaultInstance('string'); + const fieldFormats = npStart.plugins.data.fieldFormats; + + return field ? field.format : fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING); }; export class AggType { diff --git a/src/legacy/ui/public/agg_types/buckets/date_range.ts b/src/legacy/ui/public/agg_types/buckets/date_range.ts index 908d921d12313..860d76ff2aa7b 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/date_range.ts @@ -23,13 +23,15 @@ import { npStart } from 'ui/new_platform'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; -import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; import { DateRangesParamEditor } from '../../vis/editors/default/controls/date_ranges'; -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; // @ts-ignore import { dateRange } from '../../utils/date_range'; +import { + KBN_FIELD_TYPES, + TEXT_CONTEXT_TYPE, + FieldFormat, +} from '../../../../../plugins/data/public'; const dateRangeTitle = i18n.translate('common.ui.aggTypes.buckets.dateRangeTitle', { defaultMessage: 'Date Range', @@ -48,7 +50,12 @@ export const dateRangeBucketAgg = new BucketAggType({ return { from, to }; }, getFormat(agg) { - const formatter = agg.fieldOwnFormatter('text', fieldFormats.getDefaultInstance('date')); + const fieldFormats = npStart.plugins.data.fieldFormats; + + const formatter = agg.fieldOwnFormatter( + TEXT_CONTEXT_TYPE, + fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE) + ); const DateRangeFormat = FieldFormat.from(function(range: DateRangeKey) { return dateRange.toString(range, formatter); }); diff --git a/src/legacy/ui/public/agg_types/buckets/ip_range.ts b/src/legacy/ui/public/agg_types/buckets/ip_range.ts index 7ef415ff8d0c4..35155a482734c 100644 --- a/src/legacy/ui/public/agg_types/buckets/ip_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/ip_range.ts @@ -19,17 +19,20 @@ import { noop, map, omit, isNull } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { IpRangeTypeParamEditor } from '../../vis/editors/default/controls/ip_range_type'; import { IpRangesParamEditor } from '../../vis/editors/default/controls/ip_ranges'; -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; -import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; import { ipRange } from '../../utils/ip_range'; import { BUCKET_TYPES } from './bucket_agg_types'; // @ts-ignore import { createFilterIpRange } from './create_filter/ip_range'; +import { + KBN_FIELD_TYPES, + TEXT_CONTEXT_TYPE, + FieldFormat, +} from '../../../../../plugins/data/public'; const ipRangeTitle = i18n.translate('common.ui.aggTypes.buckets.ipRangeTitle', { defaultMessage: 'IPv4 Range', @@ -50,7 +53,11 @@ export const ipRangeBucketAgg = new BucketAggType({ return { type: 'range', from: bucket.from, to: bucket.to }; }, getFormat(agg) { - const formatter = agg.fieldOwnFormatter('text', fieldFormats.getDefaultInstance('ip')); + const fieldFormats = npStart.plugins.data.fieldFormats; + const formatter = agg.fieldOwnFormatter( + TEXT_CONTEXT_TYPE, + fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.IP) + ); const IpRangeFormat = FieldFormat.from(function(range: IpRangeKey) { return ipRange.toString(range, formatter); }); diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index c0f870c27f10d..6ce0b9ce38ad3 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -19,16 +19,12 @@ import chrome from 'ui/chrome'; import { noop } from 'lodash'; -import { SearchSource } from 'ui/courier'; import { i18n } from '@kbn/i18n'; +import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { AggConfigOptions } from '../agg_config'; import { IBucketAggConfig } from './_bucket_agg_type'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../courier/utils/courier_inspector_utils'; import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './inline_comp_wrapper'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; @@ -41,7 +37,7 @@ import { OtherBucketParamEditor } from '../../vis/editors/default/controls/other import { AggConfigs } from '../agg_configs'; import { Adapters } from '../../../../../plugins/inspector/public'; -import { ContentType, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; +import { ContentType, FieldFormat, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; // @ts-ignore import { Schemas } from '../../vis/editors/default/schemas'; @@ -75,7 +71,7 @@ export const termsBucketAgg = new BucketAggType({ const params = agg.params; return agg.getFieldDisplayName() + ': ' + params.order.text; }, - getFormat(bucket) { + getFormat(bucket): FieldFormat { return { getConverterFor: (type: ContentType) => { return (val: any) => { @@ -91,10 +87,11 @@ export const termsBucketAgg = new BucketAggType({ basePath: chrome.getBasePath(), }; const converter = bucket.params.field.format.getConverterFor(type); + return converter(val, undefined, undefined, parsedUrl); }; }, - }; + } as FieldFormat; }, createFilter: createFilterTerms, postFlightRequest: async ( diff --git a/src/legacy/ui/public/agg_types/metrics/cardinality.ts b/src/legacy/ui/public/agg_types/metrics/cardinality.ts index 221e1c6d6b083..301ae2c80116c 100644 --- a/src/legacy/ui/public/agg_types/metrics/cardinality.ts +++ b/src/legacy/ui/public/agg_types/metrics/cardinality.ts @@ -18,10 +18,10 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { MetricAggType } from './metric_agg_type'; -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; const uniqueCountTitle = i18n.translate('common.ui.aggTypes.metrics.uniqueCountTitle', { defaultMessage: 'Unique Count', @@ -37,7 +37,9 @@ export const cardinalityMetricAgg = new MetricAggType({ }); }, getFormat() { - return fieldFormats.getDefaultInstance('number'); + const fieldFormats = npStart.plugins.data.fieldFormats; + + return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, params: [ { diff --git a/src/legacy/ui/public/agg_types/metrics/count.ts b/src/legacy/ui/public/agg_types/metrics/count.ts index 12964c8873e97..b5b844e8658d6 100644 --- a/src/legacy/ui/public/agg_types/metrics/count.ts +++ b/src/legacy/ui/public/agg_types/metrics/count.ts @@ -18,12 +18,11 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; - export const countMetricAgg = new MetricAggType({ name: METRIC_TYPES.COUNT, title: i18n.translate('common.ui.aggTypes.metrics.countTitle', { @@ -36,7 +35,9 @@ export const countMetricAgg = new MetricAggType({ }); }, getFormat() { - return fieldFormats.getDefaultInstance('number'); + const fieldFormats = npStart.plugins.data.fieldFormats; + + return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, getValue(agg, bucket) { return bucket.doc_count; diff --git a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts index 7428bd6caa22d..c24dda180ea94 100644 --- a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts +++ b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts @@ -18,13 +18,11 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; import { METRIC_TYPES } from './metric_agg_types'; - -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; export type IMetricAggConfig = AggConfig; @@ -72,8 +70,9 @@ export class MetricAggType< this.getFormat = config.getFormat || (agg => { + const registeredFormats = npStart.plugins.data.fieldFormats; const field = agg.getField(); - return field ? field.format : fieldFormats.getDefaultInstance('number'); + return field ? field.format : registeredFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }); this.subtype = diff --git a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts index 4fabe137f1bc8..ead5122278b5a 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts @@ -18,24 +18,27 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { PercentileRanksEditor } from '../../vis/editors/default/controls/percentile_ranks'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; -import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; +import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; // required by the values editor export type IPercentileRanksAggConfig = IResponseAggConfig; +const getFieldFormats = () => npStart.plugins.data.fieldFormats; + const valueProps = { makeLabel(this: IPercentileRanksAggConfig) { + const fieldFormats = getFieldFormats(); const field = this.getField(); - const format = (field && field.format) || fieldFormats.getDefaultInstance('number'); + const format = + (field && field.format) || fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); const customLabel = this.getParam('customLabel'); const label = customLabel || this.getFieldDisplayName(); @@ -81,7 +84,11 @@ export const percentileRanksMetricAgg = new MetricAggType new ValueAggConfig(value)); }, getFormat() { - return fieldFormats.getInstance('percent') || fieldFormats.getDefaultInstance('number'); + const fieldFormats = getFieldFormats(); + return ( + fieldFormats.getInstance(FIELD_FORMAT_IDS.PERCENT) || + fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER) + ); }, getValue(agg, bucket) { return getPercentileValue(agg, bucket) / 100; diff --git a/src/legacy/ui/public/agg_types/param_types/base.ts b/src/legacy/ui/public/agg_types/param_types/base.ts index bc8ed5d485bd4..61ef73fb62e8a 100644 --- a/src/legacy/ui/public/agg_types/param_types/base.ts +++ b/src/legacy/ui/public/agg_types/param_types/base.ts @@ -20,7 +20,7 @@ import { AggParam } from '../'; import { AggConfigs } from '../agg_configs'; import { AggConfig } from '../../vis'; -import { SearchSource } from '../../courier'; +import { SearchSourceContract, FetchOptions } from '../../courier/types'; export class BaseParamType implements AggParam { name: string; @@ -55,8 +55,8 @@ export class BaseParamType implements AggParam { */ modifyAggConfigOnSearchRequestStart: ( aggConfig: AggConfig, - searchSource?: SearchSource, - options?: any + searchSource?: SearchSourceContract, + options?: FetchOptions ) => void; constructor(config: Record) { diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx index ff46b6ec34a86..10fb58783481d 100644 --- a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx @@ -35,18 +35,18 @@ import { injectUICapabilities } from './inject_ui_capabilities'; import { UICapabilitiesProvider } from './ui_capabilities_provider'; describe('injectUICapabilities', () => { - it('provides UICapabilities to SFCs', () => { - interface SFCProps { + it('provides UICapabilities to FCs', () => { + interface FCProps { uiCapabilities: UICapabilities; } - const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + const MyFC = injectUICapabilities(({ uiCapabilities }: FCProps) => { return {uiCapabilities.uiCapability2.nestedProp}; }); const wrapper = mount( - + ); diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx index 76f1dd8016313..dbc7cd03e27a3 100644 --- a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx @@ -35,18 +35,18 @@ import { injectUICapabilities } from './inject_ui_capabilities'; import { UICapabilitiesProvider } from './ui_capabilities_provider'; describe('injectUICapabilities', () => { - it('provides UICapabilities to SFCs', () => { - interface SFCProps { + it('provides UICapabilities to FCs', () => { + interface FCProps { uiCapabilities: UICapabilities; } - const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + const MyFC = injectUICapabilities(({ uiCapabilities }: FCProps) => { return {uiCapabilities.uiCapability2.nestedProp}; }); const wrapper = mount( - + ); diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx index 3871147107439..b6ffca350239c 100644 --- a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { UICapabilitiesContext } from './ui_capabilities_context'; import { capabilities } from '..'; -export const UICapabilitiesProvider: React.SFC = props => ( +export const UICapabilitiesProvider: React.FC = props => ( {props.children} diff --git a/src/legacy/ui/public/capabilities/route_setup.ts b/src/legacy/ui/public/capabilities/route_setup.ts deleted file mode 100644 index c7817b8cc5748..0000000000000 --- a/src/legacy/ui/public/capabilities/route_setup.ts +++ /dev/null @@ -1,38 +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 { get } from 'lodash'; -import chrome from 'ui/chrome'; -import uiRoutes from 'ui/routes'; -import { UICapabilities } from '.'; - -uiRoutes.addSetupWork( - (uiCapabilities: UICapabilities, kbnBaseUrl: string, $route: any, kbnUrl: any) => { - const route = get($route, 'current.$$route') as any; - if (!route.requireUICapability) { - return; - } - - if (!get(uiCapabilities, route.requireUICapability)) { - const url = chrome.addBasePath(`${kbnBaseUrl}#/home`); - kbnUrl.redirect(url); - throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN; - } - } -); diff --git a/src/legacy/ui/public/chrome/api/angular.js b/src/legacy/ui/public/chrome/api/angular.js index e6457fec93633..73d50a83e11a5 100644 --- a/src/legacy/ui/public/chrome/api/angular.js +++ b/src/legacy/ui/public/chrome/api/angular.js @@ -21,13 +21,15 @@ import { uiModules } from '../../modules'; import { directivesProvider } from '../directives'; import { registerSubUrlHooks } from './sub_url_hooks'; +import { start as data } from '../../../../core_plugins/data/public/legacy'; import { configureAppAngularModule } from 'ui/legacy_compat'; +import { npStart } from '../../new_platform/new_platform'; export function initAngularApi(chrome, internals) { chrome.setupAngular = function () { const kibana = uiModules.get('kibana'); - configureAppAngularModule(kibana); + configureAppAngularModule(kibana, npStart.core, data, false); kibana.value('chrome', chrome); diff --git a/src/legacy/ui/public/courier/fetch/call_client.js b/src/legacy/ui/public/courier/fetch/call_client.js deleted file mode 100644 index 971ae4c49a604..0000000000000 --- a/src/legacy/ui/public/courier/fetch/call_client.js +++ /dev/null @@ -1,53 +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 { groupBy } from 'lodash'; -import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; -import { handleResponse } from './handle_response'; - -export function callClient(searchRequests, requestsOptions = [], { es, config, esShardTimeout } = {}) { - // Correlate the options with the request that they're associated with - const requestOptionEntries = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); - - // Group the requests by the strategy used to search that specific request - const searchStrategyMap = groupBy(searchRequests, (request, i) => { - const searchStrategy = getSearchStrategyForSearchRequest(request, requestsOptions[i]); - return searchStrategy.id; - }); - - // Execute each search strategy with the group of requests, but return the responses in the same - // order in which they were received. We use a map to correlate the original request with its - // response. - const requestResponseMap = new Map(); - Object.keys(searchStrategyMap).forEach(searchStrategyId => { - const searchStrategy = getSearchStrategyById(searchStrategyId); - const requests = searchStrategyMap[searchStrategyId]; - const { searching, abort } = searchStrategy.search({ searchRequests: requests, es, config, esShardTimeout }); - requests.forEach((request, i) => { - const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal } = requestOptionsMap.get(request) || {}; - if (abortSignal) abortSignal.addEventListener('abort', abort); - requestResponseMap.set(request, response); - }); - }, []); - return searchRequests.map(request => requestResponseMap.get(request)); -} - - diff --git a/src/legacy/ui/public/courier/fetch/call_client.test.js b/src/legacy/ui/public/courier/fetch/call_client.test.js deleted file mode 100644 index 463d6c59e479e..0000000000000 --- a/src/legacy/ui/public/courier/fetch/call_client.test.js +++ /dev/null @@ -1,128 +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 { callClient } from './call_client'; -import { handleResponse } from './handle_response'; - -const mockResponses = [{}, {}]; -const mockAbortFns = [jest.fn(), jest.fn()]; -const mockSearchFns = [ - jest.fn(({ searchRequests }) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0] - })), - jest.fn(({ searchRequests }) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1] - })) -]; -const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); - -jest.mock('./handle_response', () => ({ - handleResponse: jest.fn((request, response) => response) -})); - -jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: request => mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: id => mockSearchStrategies[id] -})); - -describe('callClient', () => { - beforeEach(() => { - handleResponse.mockClear(); - mockAbortFns.forEach(fn => fn.mockClear()); - mockSearchFns.forEach(fn => fn.mockClear()); - }); - - test('Executes each search strategy with its group of matching requests', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; - - callClient(searchRequests); - - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([searchRequests[0], searchRequests[2]]); - expect(mockSearchFns[1]).toBeCalled(); - expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([searchRequests[1], searchRequests[3]]); - }); - - test('Passes the additional arguments it is given to the search strategy', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }]; - const args = { es: {}, config: {}, esShardTimeout: 0 }; - - callClient(searchRequests, [], args); - - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0]).toEqual({ searchRequests, ...args }); - }); - - test('Returns the responses in the original order', async () => { - const searchRequests = [{ - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }]; - - const responses = await Promise.all(callClient(searchRequests)); - - expect(responses).toEqual([mockResponses[1], mockResponses[0]]); - }); - - test('Calls handleResponse with each request and response', async () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; - - const responses = callClient(searchRequests); - await Promise.all(responses); - - expect(handleResponse).toBeCalledTimes(2); - expect(handleResponse).toBeCalledWith(searchRequests[0], mockResponses[0]); - expect(handleResponse).toBeCalledWith(searchRequests[1], mockResponses[1]); - }); - - test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; - const abortController = new AbortController(); - const requestOptions = [{ - abortSignal: abortController.signal - }]; - - callClient(searchRequests, requestOptions); - abortController.abort(); - - expect(mockAbortFns[0]).toBeCalled(); - expect(mockAbortFns[1]).not.toBeCalled(); - }); -}); diff --git a/src/legacy/ui/public/courier/fetch/call_client.test.ts b/src/legacy/ui/public/courier/fetch/call_client.test.ts new file mode 100644 index 0000000000000..74c87d77dd4fd --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/call_client.test.ts @@ -0,0 +1,121 @@ +/* + * 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 { callClient } from './call_client'; +import { handleResponse } from './handle_response'; +import { FetchHandlers, SearchRequest, SearchStrategySearchParams } from '../types'; + +const mockResponses = [{}, {}]; +const mockAbortFns = [jest.fn(), jest.fn()]; +const mockSearchFns = [ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ + searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), + abort: mockAbortFns[0], + })), + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ + searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), + abort: mockAbortFns[1], + })), +]; +const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); + +jest.mock('./handle_response', () => ({ + handleResponse: jest.fn((request, response) => response), +})); + +jest.mock('../search_strategy', () => ({ + getSearchStrategyForSearchRequest: (request: SearchRequest) => + mockSearchStrategies[request._searchStrategyId], + getSearchStrategyById: (id: number) => mockSearchStrategies[id], +})); + +describe('callClient', () => { + beforeEach(() => { + (handleResponse as jest.Mock).mockClear(); + mockAbortFns.forEach(fn => fn.mockClear()); + mockSearchFns.forEach(fn => fn.mockClear()); + }); + + test('Executes each search strategy with its group of matching requests', () => { + const searchRequests = [ + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + ]; + + callClient(searchRequests, [], {} as FetchHandlers); + + expect(mockSearchFns[0]).toBeCalled(); + expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[0], + searchRequests[2], + ]); + expect(mockSearchFns[1]).toBeCalled(); + expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[1], + searchRequests[3], + ]); + }); + + test('Passes the additional arguments it is given to the search strategy', () => { + const searchRequests = [{ _searchStrategyId: 0 }]; + const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + + callClient(searchRequests, [], args); + + expect(mockSearchFns[0]).toBeCalled(); + expect(mockSearchFns[0].mock.calls[0][0]).toEqual({ searchRequests, ...args }); + }); + + test('Returns the responses in the original order', async () => { + const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; + + const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); + + expect(responses).toEqual([mockResponses[1], mockResponses[0]]); + }); + + test('Calls handleResponse with each request and response', async () => { + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; + + const responses = callClient(searchRequests, [], {} as FetchHandlers); + await Promise.all(responses); + + expect(handleResponse).toBeCalledTimes(2); + expect(handleResponse).toBeCalledWith(searchRequests[0], mockResponses[0]); + expect(handleResponse).toBeCalledWith(searchRequests[1], mockResponses[1]); + }); + + test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; + const abortController = new AbortController(); + const requestOptions = [ + { + abortSignal: abortController.signal, + }, + ]; + + callClient(searchRequests, requestOptions, {} as FetchHandlers); + abortController.abort(); + + expect(mockAbortFns[0]).toBeCalled(); + expect(mockAbortFns[1]).not.toBeCalled(); + }); +}); diff --git a/src/legacy/ui/public/courier/fetch/call_client.ts b/src/legacy/ui/public/courier/fetch/call_client.ts new file mode 100644 index 0000000000000..43da27f941e4e --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/call_client.ts @@ -0,0 +1,69 @@ +/* + * 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 { groupBy } from 'lodash'; +import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; +import { handleResponse } from './handle_response'; +import { FetchOptions, FetchHandlers } from './types'; +import { SearchRequest } from '../types'; + +export function callClient( + searchRequests: SearchRequest[], + requestsOptions: FetchOptions[] = [], + { es, config, esShardTimeout }: FetchHandlers +) { + // Correlate the options with the request that they're associated with + const requestOptionEntries: Array<[ + SearchRequest, + FetchOptions + ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); + const requestOptionsMap = new Map(requestOptionEntries); + + // Group the requests by the strategy used to search that specific request + const searchStrategyMap = groupBy(searchRequests, (request, i) => { + const searchStrategy = getSearchStrategyForSearchRequest(request, requestsOptions[i]); + return searchStrategy.id; + }); + + // Execute each search strategy with the group of requests, but return the responses in the same + // order in which they were received. We use a map to correlate the original request with its + // response. + const requestResponseMap = new Map(); + Object.keys(searchStrategyMap).forEach(searchStrategyId => { + const searchStrategy = getSearchStrategyById(searchStrategyId); + const requests = searchStrategyMap[searchStrategyId]; + + // There's no way `searchStrategy` could be undefined here because if we didn't get a matching strategy for this ID + // then an error would have been thrown above + const { searching, abort } = searchStrategy!.search({ + searchRequests: requests, + es, + config, + esShardTimeout, + }); + + requests.forEach((request, i) => { + const response = searching.then(results => handleResponse(request, results[i])); + const { abortSignal = null } = requestOptionsMap.get(request) || {}; + if (abortSignal) abortSignal.addEventListener('abort', abort); + requestResponseMap.set(request, response); + }); + }, []); + return searchRequests.map(request => requestResponseMap.get(request)); +} diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts index de32b9d7b3087..22fc20233cc87 100644 --- a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts +++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts @@ -24,6 +24,7 @@ export interface Request { sort: unknown; stored_fields: string[]; } + export interface ResponseWithShardFailure { _shards: { failed: number; diff --git a/src/legacy/ui/public/courier/fetch/errors.ts b/src/legacy/ui/public/courier/fetch/errors.ts index aba554a795258..a2ac013915b4b 100644 --- a/src/legacy/ui/public/courier/fetch/errors.ts +++ b/src/legacy/ui/public/courier/fetch/errors.ts @@ -17,17 +17,18 @@ * under the License. */ +import { SearchError } from '../../courier'; import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { SearchResponse } from '../types'; /** * Request Failure - When an entire multi request fails * @param {Error} err - the Error that came back * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp: any; - constructor(err: any, resp?: any) { - err = err || false; - super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err.message)}`); + public resp: SearchResponse; + constructor(err: SearchError | null = null, resp?: SearchResponse) { + super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; } diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.js b/src/legacy/ui/public/courier/fetch/fetch_soon.js deleted file mode 100644 index ef02beddcb59a..0000000000000 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.js +++ /dev/null @@ -1,70 +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 { callClient } from './call_client'; - -/** - * This function introduces a slight delay in the request process to allow multiple requests to queue - * up (e.g. when a dashboard is loading). - */ -export async function fetchSoon(request, options, { es, config, esShardTimeout }) { - const delay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, delay); -} - -/** - * Delays executing a function for a given amount of time, and returns a promise that resolves - * with the result. - * @param fn The function to invoke - * @param ms The number of milliseconds to wait - * @return Promise A promise that resolves with the result of executing the function - */ -function delay(fn, ms) { - return new Promise(resolve => { - setTimeout(() => resolve(fn()), ms); - }); -} - -// The current batch/queue of requests to fetch -let requestsToFetch = []; -let requestOptions = []; - -// The in-progress fetch (if there is one) -let fetchInProgress = null; - -/** - * Delay fetching for a given amount of time, while batching up the requests to be fetched. - * Returns a promise that resolves with the response for the given request. - * @param request The request to fetch - * @param ms The number of milliseconds to wait (and batch requests) - * @return Promise The response for the given request - */ -async function delayedFetch(request, options, { es, config, esShardTimeout }, ms) { - const i = requestsToFetch.length; - requestsToFetch = [...requestsToFetch, request]; - requestOptions = [...requestOptions, options]; - const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); - requestsToFetch = []; - requestOptions = []; - fetchInProgress = null; - return response; - }, ms)); - return responses[i]; -} diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js b/src/legacy/ui/public/courier/fetch/fetch_soon.test.js deleted file mode 100644 index 824a4ab7e12e3..0000000000000 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js +++ /dev/null @@ -1,140 +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 { fetchSoon } from './fetch_soon'; -import { callClient } from './call_client'; - -function getMockConfig(config) { - const entries = Object.entries(config); - return new Map(entries); -} - -const mockResponses = { - 'foo': {}, - 'bar': {}, - 'baz': {}, -}; - -jest.useFakeTimers(); - -jest.mock('./call_client', () => ({ - callClient: jest.fn(requests => { - // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) - // in addition to how long to simulate waiting before returning a response (_waitMs) - const responses = requests.map(request => { - const waitMs = requests.reduce((total, request) => request._waitMs || 0, 0); - return new Promise(resolve => { - resolve(mockResponses[request._mockResponseId]); - }, waitMs); - }); - return Promise.resolve(responses); - }) -})); - -describe('fetchSoon', () => { - beforeEach(() => { - callClient.mockClear(); - }); - - test('should delay by 0ms if config is set to not batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': false - }); - const request = {}; - const options = {}; - - fetchSoon(request, options, { config }); - - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(0); - expect(callClient).toBeCalled(); - }); - - test('should delay by 50ms if config is set to batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': true - }); - const request = {}; - const options = {}; - - fetchSoon(request, options, { config }); - - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(0); - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(50); - expect(callClient).toBeCalled(); - }); - - test('should send a batch of requests to callClient', () => { - const config = getMockConfig({ - 'courier:batchSearches': true - }); - const requests = [{ foo: 1 }, { foo: 2 }]; - const options = [{ bar: 1 }, { bar: 2 }]; - - requests.forEach((request, i) => { - fetchSoon(request, options[i], { config }); - }); - - jest.advanceTimersByTime(50); - expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(requests); - expect(callClient.mock.calls[0][1]).toEqual(options); - }); - - test('should return the response to the corresponding call for multiple batched requests', async () => { - const config = getMockConfig({ - 'courier:batchSearches': true - }); - const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; - - const promises = requests.map(request => { - return fetchSoon(request, {}, { config }); - }); - jest.advanceTimersByTime(50); - const results = await Promise.all(promises); - - expect(results).toEqual([mockResponses.foo, mockResponses.bar]); - }); - - test('should wait for the previous batch to start before starting a new batch', () => { - const config = getMockConfig({ - 'courier:batchSearches': true - }); - const firstBatch = [{ foo: 1 }, { foo: 2 }]; - const secondBatch = [{ bar: 1 }, { bar: 2 }]; - - firstBatch.forEach(request => { - fetchSoon(request, {}, { config }); - }); - jest.advanceTimersByTime(50); - secondBatch.forEach(request => { - fetchSoon(request, {}, { config }); - }); - - expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(firstBatch); - - jest.advanceTimersByTime(50); - - expect(callClient).toBeCalledTimes(2); - expect(callClient.mock.calls[1][0]).toEqual(secondBatch); - }); -}); diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts new file mode 100644 index 0000000000000..e753c526b748d --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { fetchSoon } from './fetch_soon'; +import { callClient } from './call_client'; +import { UiSettingsClientContract } from '../../../../../core/public'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; +} + +const mockResponses: Record = { + foo: {}, + bar: {}, + baz: {}, +}; + +jest.useFakeTimers(); + +jest.mock('./call_client', () => ({ + callClient: jest.fn((requests: SearchRequest[]) => { + // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) + // in addition to how long to simulate waiting before returning a response (_waitMs) + const responses = requests.map(request => { + const waitMs = requests.reduce((total, { _waitMs }) => total + _waitMs || 0, 0); + return new Promise(resolve => { + setTimeout(() => { + resolve(mockResponses[request._mockResponseId]); + }, waitMs); + }); + }); + return Promise.resolve(responses); + }), +})); + +describe('fetchSoon', () => { + beforeEach(() => { + (callClient as jest.Mock).mockClear(); + }); + + test('should delay by 0ms if config is set to not batch searches', () => { + const config = getConfigStub({ + 'courier:batchSearches': false, + }); + const request = {}; + const options = {}; + + fetchSoon(request, options, { config } as FetchHandlers); + + expect(callClient).not.toBeCalled(); + jest.advanceTimersByTime(0); + expect(callClient).toBeCalled(); + }); + + test('should delay by 50ms if config is set to batch searches', () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + }); + const request = {}; + const options = {}; + + fetchSoon(request, options, { config } as FetchHandlers); + + expect(callClient).not.toBeCalled(); + jest.advanceTimersByTime(0); + expect(callClient).not.toBeCalled(); + jest.advanceTimersByTime(50); + expect(callClient).toBeCalled(); + }); + + test('should send a batch of requests to callClient', () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + }); + const requests = [{ foo: 1 }, { foo: 2 }]; + const options = [{ bar: 1 }, { bar: 2 }]; + + requests.forEach((request, i) => { + fetchSoon(request, options[i] as FetchOptions, { config } as FetchHandlers); + }); + + jest.advanceTimersByTime(50); + expect(callClient).toBeCalledTimes(1); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(requests); + expect((callClient as jest.Mock).mock.calls[0][1]).toEqual(options); + }); + + test('should return the response to the corresponding call for multiple batched requests', async () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + }); + const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; + + const promises = requests.map(request => { + return fetchSoon(request, {}, { config } as FetchHandlers); + }); + jest.advanceTimersByTime(50); + const results = await Promise.all(promises); + + expect(results).toEqual([mockResponses.foo, mockResponses.bar]); + }); + + test('should wait for the previous batch to start before starting a new batch', () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + }); + const firstBatch = [{ foo: 1 }, { foo: 2 }]; + const secondBatch = [{ bar: 1 }, { bar: 2 }]; + + firstBatch.forEach(request => { + fetchSoon(request, {}, { config } as FetchHandlers); + }); + jest.advanceTimersByTime(50); + secondBatch.forEach(request => { + fetchSoon(request, {}, { config } as FetchHandlers); + }); + + expect(callClient).toBeCalledTimes(1); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(firstBatch); + + jest.advanceTimersByTime(50); + + expect(callClient).toBeCalledTimes(2); + expect((callClient as jest.Mock).mock.calls[1][0]).toEqual(secondBatch); + }); +}); diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.ts new file mode 100644 index 0000000000000..75de85e02a1a2 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.ts @@ -0,0 +1,83 @@ +/* + * 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 { callClient } from './call_client'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +/** + * This function introduces a slight delay in the request process to allow multiple requests to queue + * up (e.g. when a dashboard is loading). + */ +export async function fetchSoon( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers +) { + const msToDelay = config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); +} + +/** + * Delays executing a function for a given amount of time, and returns a promise that resolves + * with the result. + * @param fn The function to invoke + * @param ms The number of milliseconds to wait + * @return Promise A promise that resolves with the result of executing the function + */ +function delay(fn: Function, ms: number) { + return new Promise(resolve => { + setTimeout(() => resolve(fn()), ms); + }); +} + +// The current batch/queue of requests to fetch +let requestsToFetch: SearchRequest[] = []; +let requestOptions: FetchOptions[] = []; + +// The in-progress fetch (if there is one) +let fetchInProgress: Promise | null = null; + +/** + * Delay fetching for a given amount of time, while batching up the requests to be fetched. + * Returns a promise that resolves with the response for the given request. + * @param request The request to fetch + * @param ms The number of milliseconds to wait (and batch requests) + * @return Promise The response for the given request + */ +async function delayedFetch( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers, + ms: number +) { + const i = requestsToFetch.length; + requestsToFetch = [...requestsToFetch, request]; + requestOptions = [...requestOptions, options]; + const responses = await (fetchInProgress = + fetchInProgress || + delay(() => { + const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + requestsToFetch = []; + requestOptions = []; + fetchInProgress = null; + return response; + }, ms)); + return responses[i]; +} diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.js b/src/legacy/ui/public/courier/fetch/get_search_params.js deleted file mode 100644 index dd55201ba5540..0000000000000 --- a/src/legacy/ui/public/courier/fetch/get_search_params.js +++ /dev/null @@ -1,58 +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. - */ - -const sessionId = Date.now(); - -export function getMSearchParams(config) { - return { - rest_total_hits_as_int: true, - ignore_throttled: getIgnoreThrottled(config), - max_concurrent_shard_requests: getMaxConcurrentShardRequests(config), - }; -} - -export function getSearchParams(config, esShardTimeout) { - return { - rest_total_hits_as_int: true, - ignore_unavailable: true, - ignore_throttled: getIgnoreThrottled(config), - max_concurrent_shard_requests: getMaxConcurrentShardRequests(config), - preference: getPreference(config), - timeout: getTimeout(esShardTimeout), - }; -} - -export function getIgnoreThrottled(config) { - return !config.get('search:includeFrozen'); -} - -export function getMaxConcurrentShardRequests(config) { - const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); - return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; -} - -export function getPreference(config) { - const setRequestPreference = config.get('courier:setRequestPreference'); - if (setRequestPreference === 'sessionId') return sessionId; - return setRequestPreference === 'custom' ? config.get('courier:customRequestPreference') : undefined; -} - -export function getTimeout(esShardTimeout) { - return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; -} diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.js b/src/legacy/ui/public/courier/fetch/get_search_params.test.js deleted file mode 100644 index 380d1da963ddf..0000000000000 --- a/src/legacy/ui/public/courier/fetch/get_search_params.test.js +++ /dev/null @@ -1,108 +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 { getMSearchParams, getSearchParams } from './get_search_params'; - -function getConfigStub(config = {}) { - return { - get: key => config[key] - }; -} - -describe('getMSearchParams', () => { - test('includes rest_total_hits_as_int', () => { - const config = getConfigStub(); - const msearchParams = getMSearchParams(config); - expect(msearchParams.rest_total_hits_as_int).toBe(true); - }); - - test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); - let msearchParams = getMSearchParams(config); - expect(msearchParams.ignore_throttled).toBe(false); - - config = getConfigStub({ 'search:includeFrozen': false }); - msearchParams = getMSearchParams(config); - expect(msearchParams.ignore_throttled).toBe(true); - }); - - test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); - let msearchParams = getMSearchParams(config); - expect(msearchParams.max_concurrent_shard_requests).toBe(undefined); - - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); - msearchParams = getMSearchParams(config); - expect(msearchParams.max_concurrent_shard_requests).toBe(5); - }); - - test('does not include other search params that are included in the msearch header or body', () => { - const config = getConfigStub({ - 'search:includeFrozen': false, - 'courier:maxConcurrentShardRequests': 5, - }); - const msearchParams = getMSearchParams(config); - expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false); - expect(msearchParams.hasOwnProperty('preference')).toBe(false); - expect(msearchParams.hasOwnProperty('timeout')).toBe(false); - }); -}); - -describe('getSearchParams', () => { - test('includes rest_total_hits_as_int', () => { - const config = getConfigStub(); - const searchParams = getSearchParams(config); - expect(searchParams.rest_total_hits_as_int).toBe(true); - }); - - test('includes ignore_unavailable', () => { - const config = getConfigStub(); - const searchParams = getSearchParams(config); - expect(searchParams.ignore_unavailable).toBe(true); - }); - - test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); - let searchParams = getSearchParams(config); - expect(searchParams.ignore_throttled).toBe(false); - - config = getConfigStub({ 'search:includeFrozen': false }); - searchParams = getSearchParams(config); - expect(searchParams.ignore_throttled).toBe(true); - }); - - test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); - let searchParams = getSearchParams(config); - expect(searchParams.max_concurrent_shard_requests).toBe(undefined); - - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); - searchParams = getSearchParams(config); - expect(searchParams.max_concurrent_shard_requests).toBe(5); - }); - - test('includes timeout according to esShardTimeout if greater than 0', () => { - const config = getConfigStub(); - let searchParams = getSearchParams(config, 0); - expect(searchParams.timeout).toBe(undefined); - - searchParams = getSearchParams(config, 100); - expect(searchParams.timeout).toBe('100ms'); - }); -}); diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.ts b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts new file mode 100644 index 0000000000000..d6f3d33099599 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { getMSearchParams, getSearchParams } from './get_search_params'; +import { UiSettingsClientContract } from '../../../../../core/public'; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; +} + +describe('getMSearchParams', () => { + test('includes rest_total_hits_as_int', () => { + const config = getConfigStub(); + const msearchParams = getMSearchParams(config); + expect(msearchParams.rest_total_hits_as_int).toBe(true); + }); + + test('includes ignore_throttled according to search:includeFrozen', () => { + let config = getConfigStub({ 'search:includeFrozen': true }); + let msearchParams = getMSearchParams(config); + expect(msearchParams.ignore_throttled).toBe(false); + + config = getConfigStub({ 'search:includeFrozen': false }); + msearchParams = getMSearchParams(config); + expect(msearchParams.ignore_throttled).toBe(true); + }); + + test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => { + let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let msearchParams = getMSearchParams(config); + expect(msearchParams.max_concurrent_shard_requests).toBe(undefined); + + config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + msearchParams = getMSearchParams(config); + expect(msearchParams.max_concurrent_shard_requests).toBe(5); + }); + + test('does not include other search params that are included in the msearch header or body', () => { + const config = getConfigStub({ + 'search:includeFrozen': false, + 'courier:maxConcurrentShardRequests': 5, + }); + const msearchParams = getMSearchParams(config); + expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false); + expect(msearchParams.hasOwnProperty('preference')).toBe(false); + expect(msearchParams.hasOwnProperty('timeout')).toBe(false); + }); +}); + +describe('getSearchParams', () => { + test('includes rest_total_hits_as_int', () => { + const config = getConfigStub(); + const searchParams = getSearchParams(config); + expect(searchParams.rest_total_hits_as_int).toBe(true); + }); + + test('includes ignore_unavailable', () => { + const config = getConfigStub(); + const searchParams = getSearchParams(config); + expect(searchParams.ignore_unavailable).toBe(true); + }); + + test('includes ignore_throttled according to search:includeFrozen', () => { + let config = getConfigStub({ 'search:includeFrozen': true }); + let searchParams = getSearchParams(config); + expect(searchParams.ignore_throttled).toBe(false); + + config = getConfigStub({ 'search:includeFrozen': false }); + searchParams = getSearchParams(config); + expect(searchParams.ignore_throttled).toBe(true); + }); + + test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests', () => { + let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let searchParams = getSearchParams(config); + expect(searchParams.max_concurrent_shard_requests).toBe(undefined); + + config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + searchParams = getSearchParams(config); + expect(searchParams.max_concurrent_shard_requests).toBe(5); + }); + + test('includes timeout according to esShardTimeout if greater than 0', () => { + const config = getConfigStub(); + let searchParams = getSearchParams(config, 0); + expect(searchParams.timeout).toBe(undefined); + + searchParams = getSearchParams(config, 100); + expect(searchParams.timeout).toBe('100ms'); + }); +}); diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.ts b/src/legacy/ui/public/courier/fetch/get_search_params.ts new file mode 100644 index 0000000000000..6b8da07ca93d4 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/get_search_params.ts @@ -0,0 +1,62 @@ +/* + * 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 { UiSettingsClientContract } from '../../../../../core/public'; + +const sessionId = Date.now(); + +export function getMSearchParams(config: UiSettingsClientContract) { + return { + rest_total_hits_as_int: true, + ignore_throttled: getIgnoreThrottled(config), + max_concurrent_shard_requests: getMaxConcurrentShardRequests(config), + }; +} + +export function getSearchParams(config: UiSettingsClientContract, esShardTimeout: number = 0) { + return { + rest_total_hits_as_int: true, + ignore_unavailable: true, + ignore_throttled: getIgnoreThrottled(config), + max_concurrent_shard_requests: getMaxConcurrentShardRequests(config), + preference: getPreference(config), + timeout: getTimeout(esShardTimeout), + }; +} + +export function getIgnoreThrottled(config: UiSettingsClientContract) { + return !config.get('search:includeFrozen'); +} + +export function getMaxConcurrentShardRequests(config: UiSettingsClientContract) { + const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); + return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; +} + +export function getPreference(config: UiSettingsClientContract) { + const setRequestPreference = config.get('courier:setRequestPreference'); + if (setRequestPreference === 'sessionId') return sessionId; + return setRequestPreference === 'custom' + ? config.get('courier:customRequestPreference') + : undefined; +} + +export function getTimeout(esShardTimeout: number) { + return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; +} diff --git a/src/legacy/ui/public/courier/fetch/handle_response.js b/src/legacy/ui/public/courier/fetch/handle_response.js deleted file mode 100644 index fb2797369d78f..0000000000000 --- a/src/legacy/ui/public/courier/fetch/handle_response.js +++ /dev/null @@ -1,67 +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 { toastNotifications } from '../../notify/toasts'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer } from '@elastic/eui'; -import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button'; - -export function handleResponse(request, response) { - if (response.timed_out) { - toastNotifications.addWarning({ - title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', { - defaultMessage: 'Data might be incomplete because your request timed out', - }), - }); - } - - if (response._shards && response._shards.failed) { - const title = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationMessage', { - defaultMessage: '{shardsFailed} of {shardsTotal} shards failed', - values: { - shardsFailed: response._shards.failed, - shardsTotal: response._shards.total, - }, - }); - const description = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationDescription', { - defaultMessage: 'The data you are seeing might be incomplete or wrong.', - }); - - const text = ( - <> - {description} - - - - ); - - toastNotifications.addWarning({ - title, - text, - }); - } - - return response; -} diff --git a/src/legacy/ui/public/courier/fetch/handle_response.test.js b/src/legacy/ui/public/courier/fetch/handle_response.test.js deleted file mode 100644 index 0836832e6c05a..0000000000000 --- a/src/legacy/ui/public/courier/fetch/handle_response.test.js +++ /dev/null @@ -1,74 +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 { handleResponse } from './handle_response'; -import { toastNotifications } from '../../notify/toasts'; - -jest.mock('../../notify/toasts', () => { - return { - toastNotifications: { - addWarning: jest.fn() - } - }; -}); - -jest.mock('@kbn/i18n', () => { - return { - i18n: { - translate: (id, { defaultMessage }) => defaultMessage - } - }; -}); - -describe('handleResponse', () => { - beforeEach(() => { - toastNotifications.addWarning.mockReset(); - }); - - test('should notify if timed out', () => { - const request = { body: {} }; - const response = { - timed_out: true - }; - const result = handleResponse(request, response); - expect(result).toBe(response); - expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('request timed out'); - }); - - test('should notify if shards failed', () => { - const request = { body: {} }; - const response = { - _shards: { - failed: true - } - }; - const result = handleResponse(request, response); - expect(result).toBe(response); - expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('shards failed'); - }); - - test('returns the response', () => { - const request = {}; - const response = {}; - const result = handleResponse(request, response); - expect(result).toBe(response); - }); -}); diff --git a/src/legacy/ui/public/courier/fetch/handle_response.test.ts b/src/legacy/ui/public/courier/fetch/handle_response.test.ts new file mode 100644 index 0000000000000..0163aca777161 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/handle_response.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { handleResponse } from './handle_response'; +import { toastNotifications } from '../../notify/toasts'; + +jest.mock('../../notify/toasts', () => { + return { + toastNotifications: { + addWarning: jest.fn(), + }, + }; +}); + +jest.mock('@kbn/i18n', () => { + return { + i18n: { + translate: (id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage, + }, + }; +}); + +describe('handleResponse', () => { + beforeEach(() => { + (toastNotifications.addWarning as jest.Mock).mockReset(); + }); + + test('should notify if timed out', () => { + const request = { body: {} }; + const response = { + timed_out: true, + }; + const result = handleResponse(request, response); + expect(result).toBe(response); + expect(toastNotifications.addWarning).toBeCalled(); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'request timed out' + ); + }); + + test('should notify if shards failed', () => { + const request = { body: {} }; + const response = { + _shards: { + failed: true, + }, + }; + const result = handleResponse(request, response); + expect(result).toBe(response); + expect(toastNotifications.addWarning).toBeCalled(); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'shards failed' + ); + }); + + test('returns the response', () => { + const request = {}; + const response = {}; + const result = handleResponse(request, response); + expect(result).toBe(response); + }); +}); diff --git a/src/legacy/ui/public/courier/fetch/handle_response.tsx b/src/legacy/ui/public/courier/fetch/handle_response.tsx new file mode 100644 index 0000000000000..d7f2263268f8c --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/handle_response.tsx @@ -0,0 +1,69 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { toastNotifications } from '../../notify/toasts'; +import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button'; +import { Request, ResponseWithShardFailure } from './components/shard_failure_types'; +import { SearchRequest, SearchResponse } from '../types'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; + +export function handleResponse(request: SearchRequest, response: SearchResponse) { + if (response.timed_out) { + toastNotifications.addWarning({ + title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', { + defaultMessage: 'Data might be incomplete because your request timed out', + }), + }); + } + + if (response._shards && response._shards.failed) { + const title = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationMessage', { + defaultMessage: '{shardsFailed} of {shardsTotal} shards failed', + values: { + shardsFailed: response._shards.failed, + shardsTotal: response._shards.total, + }, + }); + const description = i18n.translate( + 'common.ui.courier.fetch.shardsFailedNotificationDescription', + { + defaultMessage: 'The data you are seeing might be incomplete or wrong.', + } + ); + + const text = toMountPoint( + <> + {description} + + + + ); + + toastNotifications.addWarning({ title, text }); + } + + return response; +} diff --git a/src/legacy/ui/public/courier/fetch/index.js b/src/legacy/ui/public/courier/fetch/index.ts similarity index 100% rename from src/legacy/ui/public/courier/fetch/index.js rename to src/legacy/ui/public/courier/fetch/index.ts diff --git a/src/legacy/ui/public/courier/fetch/types.ts b/src/legacy/ui/public/courier/fetch/types.ts new file mode 100644 index 0000000000000..e341e1ab35c5c --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/types.ts @@ -0,0 +1,41 @@ +/* + * 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 { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface ApiCaller { + search: (searchRequest: SearchRequest) => ApiCallerResponse; + msearch: (searchRequest: SearchRequest) => ApiCallerResponse; +} + +export interface ApiCallerResponse extends Promise { + abort: () => void; +} + +export interface FetchOptions { + abortSignal?: AbortSignal; + searchStrategyId?: string; +} + +export interface FetchHandlers { + es: ApiCaller; + config: UiSettingsClientContract; + esShardTimeout: number; +} diff --git a/src/legacy/ui/public/courier/index.d.ts b/src/legacy/ui/public/courier/index.d.ts deleted file mode 100644 index 93556c2666c9a..0000000000000 --- a/src/legacy/ui/public/courier/index.d.ts +++ /dev/null @@ -1,22 +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. - */ - -export * from './search_source'; -export * from './search_strategy'; -export * from './utils/courier_inspector_utils'; diff --git a/src/legacy/ui/public/courier/index.js b/src/legacy/ui/public/courier/index.js deleted file mode 100644 index 5647af3d0d645..0000000000000 --- a/src/legacy/ui/public/courier/index.js +++ /dev/null @@ -1,28 +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. - */ - -export { SearchSource } from './search_source'; - -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - isDefaultTypeIndexPattern, - SearchError, - getSearchErrorType, -} from './search_strategy'; diff --git a/src/legacy/ui/public/courier/index.ts b/src/legacy/ui/public/courier/index.ts new file mode 100644 index 0000000000000..3c16926d2aba7 --- /dev/null +++ b/src/legacy/ui/public/courier/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './fetch'; +export * from './search_source'; +export * from './search_strategy'; +export * from './utils/courier_inspector_utils'; +export * from './types'; diff --git a/src/legacy/ui/public/courier/search_poll/search_poll.js b/src/legacy/ui/public/courier/search_poll/search_poll.js deleted file mode 100644 index f00c2a32e0ec6..0000000000000 --- a/src/legacy/ui/public/courier/search_poll/search_poll.js +++ /dev/null @@ -1,68 +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 _ from 'lodash'; - -import { timefilter } from 'ui/timefilter'; - -export class SearchPoll { - constructor() { - this._isPolling = false; - this._intervalInMs = undefined; - this._timerId = null; - } - - setIntervalInMs = intervalInMs => { - this._intervalInMs = _.parseInt(intervalInMs); - }; - - resume = () => { - this._isPolling = true; - this.resetTimer(); - }; - - pause = () => { - this._isPolling = false; - this.clearTimer(); - }; - - resetTimer = () => { - // Cancel the pending search and schedule a new one. - this.clearTimer(); - - if (this._isPolling) { - this._timerId = setTimeout(this._search, this._intervalInMs); - } - }; - - clearTimer = () => { - // Cancel the pending search, if there is one. - if (this._timerId) { - clearTimeout(this._timerId); - this._timerId = null; - } - }; - - _search = () => { - // Schedule another search. - this.resetTimer(); - - timefilter.notifyShouldFetch(); - }; -} diff --git a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js deleted file mode 100644 index 279e389dec114..0000000000000 --- a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js +++ /dev/null @@ -1,124 +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 '../../../private'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { normalizeSortRequest } from '../_normalize_sort_request'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import _ from 'lodash'; - -describe('SearchSource#normalizeSortRequest', function () { - let indexPattern; - let normalizedSort; - const defaultSortOptions = { unmapped_type: 'boolean' }; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - normalizedSort = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - })); - - it('should return an array', function () { - const sortable = { someField: 'desc' }; - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(result).to.be.an(Array); - expect(result).to.eql(normalizedSort); - // ensure object passed in is not mutated - expect(result[0]).to.not.be.equal(sortable); - expect(sortable).to.eql({ someField: 'desc' }); - }); - - it('should make plain string sort into the more verbose format', function () { - const result = normalizeSortRequest([{ someField: 'desc' }], indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should append default sort options', function () { - const sortState = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - const result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should enable script based sorting', function () { - const fieldName = 'script string'; - const direction = 'desc'; - const indexField = indexPattern.fields.getByName(fieldName); - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = { - _script: { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: indexField.type, - order: direction - } - }; - - let result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - - sortState[fieldName] = { order: direction }; - result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - }); - - it('should use script based sorting only on sortable types', function () { - const fieldName = 'script murmur3'; - const direction = 'asc'; - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = {}; - normalizedSort[fieldName] = { - order: direction, - unmapped_type: 'boolean' - }; - const result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - - expect(result).to.eql([normalizedSort]); - }); - - it('should remove unmapped_type parameter from _score sorting', function () { - const sortable = { _score: 'desc' }; - const expected = [{ - _score: { - order: 'desc' - } - }]; - - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(_.isEqual(result, expected)).to.be.ok(); - - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js deleted file mode 100644 index 3e5d7a1374115..0000000000000 --- a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js +++ /dev/null @@ -1,88 +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 _ from 'lodash'; - -/** - * Decorate queries with default parameters - * @param {query} query object - * @returns {object} - */ -export function normalizeSortRequest(sortObject, indexPattern, defaultSortOptions) { - // [].concat({}) -> [{}], [].concat([{}]) -> [{}] - return [].concat(sortObject).map(function (sortable) { - return normalize(sortable, indexPattern, defaultSortOptions); - }); -} - -/* - Normalize the sort description to the more verbose format: - { someField: "desc" } into { someField: { "order": "desc"}} - */ -function normalize(sortable, indexPattern, defaultSortOptions) { - const normalized = {}; - let sortField = _.keys(sortable)[0]; - let sortValue = sortable[sortField]; - const indexField = indexPattern.fields.getByName(sortField); - - if (indexField && indexField.scripted && indexField.sortable) { - let direction; - if (_.isString(sortValue)) direction = sortValue; - if (_.isObject(sortValue) && sortValue.order) direction = sortValue.order; - - sortField = '_script'; - sortValue = { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: castSortType(indexField.type), - order: direction - }; - } else { - if (_.isString(sortValue)) { - sortValue = { order: sortValue }; - } - sortValue = _.defaults({}, sortValue, defaultSortOptions); - - if (sortField === '_score') { - delete sortValue.unmapped_type; - } - } - - normalized[sortField] = sortValue; - return normalized; -} - -// The ES API only supports sort scripts of type 'number' and 'string' -function castSortType(type) { - const typeCastings = { - number: 'number', - string: 'string', - date: 'number', - boolean: 'string' - }; - - const castedType = typeCastings[type]; - if (!castedType) { - throw new Error(`Unsupported script sort type: ${type}`); - } - - return castedType; -} diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js deleted file mode 100644 index cd726709b4b5c..0000000000000 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js +++ /dev/null @@ -1,25 +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. - */ - -export function filterDocvalueFields(docvalueFields, fields) { - return docvalueFields.filter(docValue => { - const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; - return fields.includes(docvalueFieldName); - }); -} diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js deleted file mode 100644 index b220361e33b3b..0000000000000 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js +++ /dev/null @@ -1,33 +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 { filterDocvalueFields } from './filter_docvalue_fields'; - -test('Should exclude docvalue_fields that are not contained in fields', () => { - const docvalueFields = [ - 'my_ip_field', - { field: 'my_keyword_field' }, - { field: 'my_date_field', 'format': 'epoch_millis' } - ]; - const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); - expect(out).toEqual([ - 'my_ip_field', - { field: 'my_keyword_field' }, - ]); -}); diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts new file mode 100644 index 0000000000000..522117fe22804 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { filterDocvalueFields } from './filter_docvalue_fields'; + +test('Should exclude docvalue_fields that are not contained in fields', () => { + const docvalueFields = [ + 'my_ip_field', + { field: 'my_keyword_field' }, + { field: 'my_date_field', format: 'epoch_millis' }, + ]; + const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); + expect(out).toEqual(['my_ip_field', { field: 'my_keyword_field' }]); +}); diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts new file mode 100644 index 0000000000000..917d26f0decd1 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ + +interface DocvalueField { + field: string; + [key: string]: unknown; +} + +export function filterDocvalueFields( + docvalueFields: Array, + fields: string[] +) { + return docvalueFields.filter(docValue => { + const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; + return fields.includes(docvalueFieldName); + }); +} diff --git a/src/legacy/ui/public/courier/search_source/index.d.ts b/src/legacy/ui/public/courier/search_source/index.d.ts deleted file mode 100644 index dcae7b3d2ff05..0000000000000 --- a/src/legacy/ui/public/courier/search_source/index.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export { SearchSource } from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/index.js b/src/legacy/ui/public/courier/search_source/index.js deleted file mode 100644 index dcae7b3d2ff05..0000000000000 --- a/src/legacy/ui/public/courier/search_source/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { SearchSource } from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/index.ts b/src/legacy/ui/public/courier/search_source/index.ts new file mode 100644 index 0000000000000..72170adc2b129 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/mocks.ts b/src/legacy/ui/public/courier/search_source/mocks.ts index bf546c1b9e7c2..2b83f379b4f09 100644 --- a/src/legacy/ui/public/courier/search_source/mocks.ts +++ b/src/legacy/ui/public/courier/search_source/mocks.ts @@ -36,21 +36,22 @@ * under the License. */ -export const searchSourceMock = { +import { SearchSourceContract } from './search_source'; + +export const searchSourceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), - getPreferredSearchStrategyId: jest.fn(), - setFields: jest.fn(), - setField: jest.fn(), + setFields: jest.fn().mockReturnThis(), + setField: jest.fn().mockReturnThis(), getId: jest.fn(), getFields: jest.fn(), getField: jest.fn(), getOwnField: jest.fn(), - create: jest.fn(), - createCopy: jest.fn(), - createChild: jest.fn(), + create: jest.fn().mockReturnThis(), + createCopy: jest.fn().mockReturnThis(), + createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), - getParent: jest.fn(), - fetch: jest.fn(), + getParent: jest.fn().mockReturnThis(), + fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), destroy: jest.fn(), diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts new file mode 100644 index 0000000000000..d27b01eb5cf7c --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { normalizeSortRequest } from './normalize_sort_request'; +import { SortDirection } from './types'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +jest.mock('ui/new_platform'); + +describe('SearchSource#normalizeSortRequest', function() { + const scriptedField = { + name: 'script string', + type: 'number', + scripted: true, + sortable: true, + script: 'foo', + lang: 'painless', + }; + const murmurScriptedField = { + ...scriptedField, + sortable: false, + name: 'murmur script', + type: 'murmur3', + }; + const indexPattern = { + fields: [scriptedField, murmurScriptedField], + } as IndexPattern; + + it('should return an array', function() { + const sortable = { someField: SortDirection.desc }; + const result = normalizeSortRequest(sortable, indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + // ensure object passed in is not mutated + expect(result[0]).not.toBe(sortable); + expect(sortable).toEqual({ someField: SortDirection.desc }); + }); + + it('should make plain string sort into the more verbose format', function() { + const result = normalizeSortRequest([{ someField: SortDirection.desc }], indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should append default sort options', function() { + const defaultSortOptions = { + unmapped_type: 'boolean', + }; + const result = normalizeSortRequest( + [{ someField: SortDirection.desc }], + indexPattern, + defaultSortOptions + ); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + ...defaultSortOptions, + }, + }, + ]); + }); + + it('should enable script based sorting', function() { + const result = normalizeSortRequest( + { + [scriptedField.name]: SortDirection.desc, + }, + indexPattern + ); + expect(result).toEqual([ + { + _script: { + script: { + source: scriptedField.script, + lang: scriptedField.lang, + }, + type: scriptedField.type, + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should use script based sorting only on sortable types', function() { + const result = normalizeSortRequest( + [ + { + [murmurScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + [murmurScriptedField.name]: { + order: SortDirection.asc, + }, + }, + ]); + }); + + it('should remove unmapped_type parameter from _score sorting', function() { + const result = normalizeSortRequest({ _score: SortDirection.desc }, indexPattern, { + unmapped_type: 'boolean', + }); + expect(result).toEqual([ + { + _score: { + order: SortDirection.desc, + }, + }, + ]); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts new file mode 100644 index 0000000000000..0f8fc8076caa0 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts @@ -0,0 +1,78 @@ +/* + * 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 { IndexPattern } from '../../../../core_plugins/data/public'; +import { EsQuerySortValue, SortOptions } from './types'; + +export function normalizeSortRequest( + sortObject: EsQuerySortValue | EsQuerySortValue[], + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: SortOptions = {} +) { + const sortArray: EsQuerySortValue[] = Array.isArray(sortObject) ? sortObject : [sortObject]; + return sortArray.map(function(sortable) { + return normalize(sortable, indexPattern, defaultSortOptions); + }); +} + +/** + * Normalize the sort description to the more verbose format (e.g. { someField: "desc" } into + * { someField: { "order": "desc"}}), and convert sorts on scripted fields into the proper script + * for Elasticsearch. Mix in the default options according to the advanced settings. + */ +function normalize( + sortable: EsQuerySortValue, + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: any +) { + const [[sortField, sortOrder]] = Object.entries(sortable); + const order = typeof sortOrder === 'object' ? sortOrder : { order: sortOrder }; + + if (indexPattern && typeof indexPattern !== 'string') { + const indexField = indexPattern.fields.find(({ name }) => name === sortField); + if (indexField && indexField.scripted && indexField.sortable) { + return { + _script: { + script: { + source: indexField.script, + lang: indexField.lang, + }, + type: castSortType(indexField.type), + ...order, + }, + }; + } + } + + // Don't include unmapped_type for _score field + const { unmapped_type, ...otherSortOptions } = defaultSortOptions; + return { + [sortField]: { ...order, ...(sortField === '_score' ? otherSortOptions : defaultSortOptions) }, + }; +} + +// The ES API only supports sort scripts of type 'number' and 'string' +function castSortType(type: string) { + if (['number', 'string'].includes(type)) { + return 'number'; + } else if (['string', 'boolean'].includes(type)) { + return 'string'; + } + throw new Error(`Unsupported script sort type: ${type}`); +} diff --git a/src/legacy/ui/public/courier/search_source/search_source.d.ts b/src/legacy/ui/public/courier/search_source/search_source.d.ts deleted file mode 100644 index 674e7ace0594c..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.d.ts +++ /dev/null @@ -1,39 +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. - */ - -export declare class SearchSource { - setPreferredSearchStrategyId: (searchStrategyId: string) => void; - getPreferredSearchStrategyId: () => string; - setFields: (newFields: any) => SearchSource; - setField: (field: string, value: any) => SearchSource; - getId: () => string; - getFields: () => any; - getField: (field: string) => any; - getOwnField: () => any; - create: () => SearchSource; - createCopy: () => SearchSource; - createChild: (options?: any) => SearchSource; - setParent: (parent: SearchSource | boolean) => SearchSource; - getParent: () => SearchSource | undefined; - fetch: (options?: any) => Promise; - onRequestStart: (handler: (searchSource: SearchSource, options: any) => void) => void; - getSearchRequestBody: () => any; - destroy: () => void; - history: any[]; -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.js b/src/legacy/ui/public/courier/search_source/search_source.js deleted file mode 100644 index bc69e862fea48..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.js +++ /dev/null @@ -1,540 +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. - */ - -/** - * @name SearchSource - * - * @description A promise-based stream of search results that can inherit from other search sources. - * - * Because filters/queries in Kibana have different levels of persistence and come from different - * places, it is important to keep track of where filters come from for when they are saved back to - * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects - * that can have associated query parameters (index, query, filter, etc) which can also inherit from - * other searchSource objects. - * - * At query time, all of the searchSource objects that have subscribers are "flattened", at which - * point the query params from the searchSource are collected while traversing up the inheritance - * chain. At each link in the chain a decision about how to merge the query params is made until a - * single set of query parameters is created for each active searchSource (a searchSource with - * subscribers). - * - * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy - * works in Kibana. - * - * Visualize, starting from a new search: - * - * - the `savedVis.searchSource` is set as the `appSearchSource`. - * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is - * upgraded to inherit from the `rootSearchSource`. - * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so - * they will be stored directly on the `savedVis.searchSource`. - * - Any interaction with the time filter will be written to the `rootSearchSource`, so those - * filters will not be saved by the `savedVis`. - * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are - * defined on it directly, but none of the ones that it inherits from other places. - * - * Visualize, starting from an existing search: - * - * - The `savedVis` loads the `savedSearch` on which it is built. - * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as - * the `appSearchSource`. - * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. - * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the - * filters created in the visualize application and will reconnect the filters from the - * `savedSearch` at runtime to prevent losing the relationship - * - * Dashboard search sources: - * - * - Each panel in a dashboard has a search source. - * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. - * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from - * the dashboard search source. - * - When a filter is added to the search box, or via a visualization, it is written to the - * `appSearchSource`. - */ - -import _ from 'lodash'; -import angular from 'angular'; - -import { normalizeSortRequest } from './_normalize_sort_request'; - -import { fetchSoon } from '../fetch'; -import { fieldWildcardFilter } from '../../field_wildcard'; -import { getHighlightRequest, esQuery } from '../../../../../plugins/data/public'; -import { npSetup } from 'ui/new_platform'; -import chrome from '../../chrome'; -import { RequestFailure } from '../fetch/errors'; -import { filterDocvalueFields } from './filter_docvalue_fields'; - -const FIELDS = [ - 'type', - 'query', - 'filter', - 'sort', - 'highlight', - 'highlightAll', - 'aggs', - 'from', - 'searchAfter', - 'size', - 'source', - 'version', - 'fields', - 'index', -]; - -function parseInitialFields(initialFields) { - if (!initialFields) { - return {}; - } - - return typeof initialFields === 'string' ? - JSON.parse(initialFields) - : _.cloneDeep(initialFields); -} - -function isIndexPattern(val) { - return Boolean(val && typeof val.title === 'string'); -} - -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout'); -const config = npSetup.core.uiSettings; -const getConfig = (...args) => config.get(...args); -const forIp = Symbol('for which index pattern?'); - -export class SearchSource { - constructor(initialFields) { - this._id = _.uniqueId('data_source'); - - this._searchStrategyId = undefined; - this._fields = parseInitialFields(initialFields); - this._parent = undefined; - - this.history = []; - this._requestStartHandlers = []; - this._inheritOptions = {}; - } - - /***** - * PUBLIC API - *****/ - - setPreferredSearchStrategyId(searchStrategyId) { - this._searchStrategyId = searchStrategyId; - } - - getPreferredSearchStrategyId() { - return this._searchStrategyId; - } - - setFields(newFields) { - this._fields = newFields; - return this; - } - - setField(field, value) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't set field '${field}' on SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - if (field === 'index') { - const fields = this._fields; - - const hasSource = fields.source; - const sourceCameFromIp = hasSource && fields.source.hasOwnProperty(forIp); - const sourceIsForOurIp = sourceCameFromIp && fields.source[forIp] === fields.index; - if (sourceIsForOurIp) { - delete fields.source; - } - - if (value === null || value === undefined) { - delete fields.index; - return this; - } - - if (!isIndexPattern(value)) { - throw new TypeError('expected indexPattern to be an IndexPattern duck.'); - } - - fields[field] = value; - if (!fields.source) { - // imply source filtering based on the index pattern, but allow overriding - // it by simply setting another field for "source". When index is changed - fields.source = function () { - return value.getSourceFiltering(); - }; - fields.source[forIp] = value; - } - - return this; - } - - if (value == null) { - delete this._fields[field]; - return this; - } - - this._fields[field] = value; - return this; - } - - getId() { - return this._id; - } - - getFields() { - return _.clone(this._fields); - } - - /** - * Get fields from the fields - */ - getField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - let searchSource = this; - - while (searchSource) { - const value = searchSource._fields[field]; - if (value !== void 0) { - return value; - } - - searchSource = searchSource.getParent(); - } - } - - /** - * Get the field from our own fields, don't traverse up the chain - */ - getOwnField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - const value = this._fields[field]; - if (value !== void 0) { - return value; - } - } - - create() { - return new SearchSource(); - } - - createCopy() { - const json = angular.toJson(this._fields); - const newSearchSource = new SearchSource(json); - // when serializing the internal fields we lose the internal classes used in the index - // pattern, so we have to set it again to workaround this behavior - newSearchSource.setField('index', this.getField('index')); - newSearchSource.setParent(this.getParent()); - return newSearchSource; - } - - createChild(options = {}) { - const childSearchSource = new SearchSource(); - childSearchSource.setParent(this, options); - return childSearchSource; - } - - /** - * Set a searchSource that this source should inherit from - * @param {SearchSource} searchSource - the parent searchSource - * @return {this} - chainable - */ - setParent(parent, options = {}) { - this._parent = parent; - this._inheritOptions = options; - return this; - } - - /** - * Get the parent of this SearchSource - * @return {undefined|searchSource} - */ - getParent() { - return this._parent || undefined; - } - - /** - * Fetch this source and reject the returned Promise on error - * - * @async - */ - async fetch(options) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es'); - - await this.requestIsStarting(options); - - const searchRequest = await this._flatten(); - this.history = [searchRequest]; - - const response = await fetchSoon(searchRequest, { - ...(this._searchStrategyId && { searchStrategyId: this._searchStrategyId }), - ...options, - }, { es, config, esShardTimeout }); - - if (response.error) { - throw new RequestFailure(null, response); - } - - return response; - } - - /** - * Add a handler that will be notified whenever requests start - * @param {Function} handler - * @return {undefined} - */ - onRequestStart(handler) { - this._requestStartHandlers.push(handler); - } - - /** - * Called by requests of this search source when they are started - * @param {Courier.Request} request - * @param options - * @return {Promise} - */ - requestIsStarting(options) { - const handlers = [...this._requestStartHandlers]; - // If callparentStartHandlers has been set to true, we also call all - // handlers of parent search sources. - if (this._inheritOptions.callParentStartHandlers) { - let searchSource = this.getParent(); - while (searchSource) { - handlers.push(...searchSource._requestStartHandlers); - searchSource = searchSource.getParent(); - } - } - - return Promise.all(handlers.map(fn => fn(this, options))); - } - - async getSearchRequestBody() { - const searchRequest = await this._flatten(); - return searchRequest.body; - } - - /** - * Completely destroy the SearchSource. - * @return {undefined} - */ - destroy() { - this._requestStartHandlers.length = 0; - } - - /****** - * PRIVATE APIS - ******/ - - /** - * Used to merge properties into the data within ._flatten(). - * The data is passed in and modified by the function - * - * @param {object} data - the current merged data - * @param {*} val - the value at `key` - * @param {*} key - The key of `val` - * @return {undefined} - */ - _mergeProp(data, val, key) { - if (typeof val === 'function') { - const source = this; - return Promise.resolve(val(this)) - .then(function (newVal) { - return source._mergeProp(data, newVal, key); - }); - } - - if (val == null || !key || !_.isString(key)) return; - - switch (key) { - case 'filter': - const filters = Array.isArray(val) ? val : [val]; - data.filters = [...(data.filters || []), ...filters]; - return; - case 'index': - case 'type': - case 'id': - case 'highlightAll': - if (key && data[key] == null) { - data[key] = val; - } - return; - case 'searchAfter': - key = 'search_after'; - addToBody(); - break; - case 'source': - key = '_source'; - addToBody(); - break; - case 'sort': - val = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); - addToBody(); - break; - case 'query': - data.query = (data.query || []).concat(val); - break; - case 'fields': - data[key] = _.uniq([...(data[key] || []), ...val]); - break; - default: - addToBody(); - } - - /** - * Add the key and val to the body of the request - */ - function addToBody() { - data.body = data.body || {}; - // ignore if we already have a value - if (data.body[key] == null) { - data.body[key] = val; - } - } - } - - /** - * Walk the inheritance chain of a source and return it's - * flat representation (taking into account merging rules) - * @returns {Promise} - * @resolved {Object|null} - the flat data of the SearchSource - */ - _flatten() { - // the merged data of this dataSource and it's ancestors - const flatData = {}; - - // function used to write each property from each data object in the chain to flat data - const root = this; - - // start the chain at this source - let current = this; - - // call the ittr and return it's promise - return (function ittr() { - // iterate the _fields object (not array) and - // pass each key:value pair to source._mergeProp. if _mergeProp - // returns a promise, then wait for it to complete and call _mergeProp again - return Promise.all(_.map(current._fields, function ittr(value, key) { - if (value instanceof Promise) { - return value.then(function (value) { - return ittr(value, key); - }); - } - - const prom = root._mergeProp(flatData, value, key); - return prom instanceof Promise ? prom : null; - })) - .then(function () { - // move to this sources parent - const parent = current.getParent(); - // keep calling until we reach the top parent - if (parent) { - current = parent; - return ittr(); - } - }); - }()) - .then(function () { - // This is down here to prevent the circular dependency - flatData.body = flatData.body || {}; - - const computedFields = flatData.index.getComputedFields(); - - flatData.body.stored_fields = computedFields.storedFields; - flatData.body.script_fields = flatData.body.script_fields || {}; - _.extend(flatData.body.script_fields, computedFields.scriptFields); - - const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields : []; - flatData.body.docvalue_fields = flatData.body.docvalue_fields || defaultDocValueFields; - - if (flatData.body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(flatData.body._source.excludes, config.get('metaFields')); - flatData.body.docvalue_fields = flatData.body.docvalue_fields.filter( - docvalueField => filter(docvalueField.field) - ); - } - - // if we only want to search for certain fields - const fields = flatData.fields; - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - flatData.body.docvalue_fields = filterDocvalueFields(flatData.body.docvalue_fields, fields); - flatData.body.script_fields = _.pick(flatData.body.script_fields, fields); - - // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(flatData.body.script_fields)); - flatData.body.stored_fields = remainingFields; - _.set(flatData.body, '_source.includes', remainingFields); - } - - const esQueryConfigs = esQuery.getEsQueryConfig(config); - flatData.body.query = esQuery.buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs); - - if (flatData.highlightAll != null) { - if (flatData.highlightAll && flatData.body.query) { - flatData.body.highlight = getHighlightRequest(flatData.body.query, getConfig); - } - delete flatData.highlightAll; - } - - /** - * Translate a filter into a query to support es 3+ - * @param {Object} filter - The filter to translate - * @return {Object} the query version of that filter - */ - const translateToQuery = function (filter) { - if (!filter) return; - - if (filter.query) { - return filter.query; - } - - return filter; - }; - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function (id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const filters = agg.filters.filters; - - Object.keys(filters).forEach(function (filterId) { - filters[filterId] = translateToQuery(filters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - }(flatData.body.aggs || flatData.body.aggregations)); - - return flatData; - }); - } -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.js b/src/legacy/ui/public/courier/search_source/search_source.test.js deleted file mode 100644 index 800f4e4308671..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.test.js +++ /dev/null @@ -1,193 +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 { SearchSource } from '../search_source'; - -jest.mock('ui/new_platform', () => ({ - npSetup: { - core: { - injectedMetadata: { - getInjectedVar: () => 0, - } - } - } -})); - -jest.mock('../fetch', () => ({ - fetchSoon: jest.fn(), -})); - -const indexPattern = { title: 'foo' }; -const indexPattern2 = { title: 'foo' }; - -describe('SearchSource', function () { - describe('#setField()', function () { - it('sets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.setField('index', 5)).toThrow(); - }); - }); - - describe('#getField()', function () { - it('gets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.getField('unacceptablePropName')).toThrow(); - }); - }); - - describe(`#setField('index')`, function () { - describe('auto-sourceFiltering', function () { - describe('new index pattern assigned', function () { - it('generates a searchSource filter', function () { - const searchSource = new SearchSource(); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('new index pattern assigned over another', function () { - it('replaces searchSource filter with new', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - const searchSourceFilter1 = searchSource.getField('source'); - searchSource.setField('index', indexPattern2); - expect(searchSource.getField('index')).toBe(indexPattern2); - expect(typeof searchSource.getField('source')).toBe('function'); - expect(searchSource.getField('source')).not.toBe(searchSourceFilter1); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('ip assigned before custom searchSource filter', function () { - it('custom searchSource filter becomes new searchSource', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - searchSource.setField('source', football); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - searchSource.setField('source', football); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - - describe('ip assigned after custom searchSource filter', function () { - it('leaves the custom filter in place', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - }); - }); - - describe('#onRequestStart()', () => { - it('should be called when starting a request', () => { - const searchSource = new SearchSource(); - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const options = {}; - searchSource.requestIsStarting(options); - expect(fn).toBeCalledWith(searchSource, options); - }); - - it('should not be called on parent searchSource', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).not.toBeCalled(); - }); - - it('should be called on parent searchSource if callParentStartHandlers is true', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent, { callParentStartHandlers: true }); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).toBeCalledWith(searchSource, options); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.ts b/src/legacy/ui/public/courier/search_source/search_source.test.ts new file mode 100644 index 0000000000000..ddd3717f55e29 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { SearchSource } from '../search_source'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +jest.mock('ui/new_platform'); + +jest.mock('../fetch', () => ({ + fetchSoon: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../chrome', () => ({ + dangerouslyGetActiveInjector: () => ({ + get: jest.fn(), + }), +})); + +const getComputedFields = () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], +}); +const mockSource = { excludes: ['foo-*'] }; +const mockSource2 = { excludes: ['bar-*'] }; +const indexPattern = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource, +} as unknown) as IndexPattern; +const indexPattern2 = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource2, +} as unknown) as IndexPattern; + +describe('SearchSource', function() { + describe('#setField()', function() { + it('sets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe('#getField()', function() { + it('gets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe(`#setField('index')`, function() { + describe('auto-sourceFiltering', function() { + describe('new index pattern assigned', function() { + it('generates a searchSource filter', async function() { + const searchSource = new SearchSource(); + expect(searchSource.getField('index')).toBe(undefined); + expect(searchSource.getField('source')).toBe(undefined); + searchSource.setField('index', indexPattern); + expect(searchSource.getField('index')).toBe(indexPattern); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + + describe('new index pattern assigned over another', function() { + it('replaces searchSource filter with new', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + expect(searchSource.getField('index')).toBe(indexPattern2); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource2); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + }); + }); + + describe('#onRequestStart()', () => { + it('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }); + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const options = {}; + await searchSource.fetch(options); + expect(fn).toBeCalledWith(searchSource, options); + }); + + it('should not be called on parent searchSource', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).not.toBeCalled(); + }); + + it('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + callParentStartHandlers: true, + }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).toBeCalledWith(searchSource, options); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.ts b/src/legacy/ui/public/courier/search_source/search_source.ts new file mode 100644 index 0000000000000..e862bb1118a74 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.ts @@ -0,0 +1,410 @@ +/* + * 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. + */ + +/** + * @name SearchSource + * + * @description A promise-based stream of search results that can inherit from other search sources. + * + * Because filters/queries in Kibana have different levels of persistence and come from different + * places, it is important to keep track of where filters come from for when they are saved back to + * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects + * that can have associated query parameters (index, query, filter, etc) which can also inherit from + * other searchSource objects. + * + * At query time, all of the searchSource objects that have subscribers are "flattened", at which + * point the query params from the searchSource are collected while traversing up the inheritance + * chain. At each link in the chain a decision about how to merge the query params is made until a + * single set of query parameters is created for each active searchSource (a searchSource with + * subscribers). + * + * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy + * works in Kibana. + * + * Visualize, starting from a new search: + * + * - the `savedVis.searchSource` is set as the `appSearchSource`. + * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is + * upgraded to inherit from the `rootSearchSource`. + * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so + * they will be stored directly on the `savedVis.searchSource`. + * - Any interaction with the time filter will be written to the `rootSearchSource`, so those + * filters will not be saved by the `savedVis`. + * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are + * defined on it directly, but none of the ones that it inherits from other places. + * + * Visualize, starting from an existing search: + * + * - The `savedVis` loads the `savedSearch` on which it is built. + * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as + * the `appSearchSource`. + * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. + * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the + * filters created in the visualize application and will reconnect the filters from the + * `savedSearch` at runtime to prevent losing the relationship + * + * Dashboard search sources: + * + * - Each panel in a dashboard has a search source. + * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. + * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from + * the dashboard search source. + * - When a filter is added to the search box, or via a visualization, it is written to the + * `appSearchSource`. + */ + +import _ from 'lodash'; +import { npSetup } from 'ui/new_platform'; +import { normalizeSortRequest } from './normalize_sort_request'; +import { fetchSoon } from '../fetch'; +import { fieldWildcardFilter } from '../../field_wildcard'; +import { getHighlightRequest, esFilters, esQuery } from '../../../../../plugins/data/public'; +import chrome from '../../chrome'; +import { RequestFailure } from '../fetch/errors'; +import { filterDocvalueFields } from './filter_docvalue_fields'; +import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; +import { FetchOptions, ApiCaller } from '../fetch/types'; + +const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; +const config = npSetup.core.uiSettings; + +export type SearchSourceContract = Pick; + +export class SearchSource { + private id: string = _.uniqueId('data_source'); + private searchStrategyId?: string; + private parent?: SearchSource; + private requestStartHandlers: Array< + (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + > = []; + private inheritOptions: SearchSourceOptions = {}; + public history: SearchRequest[] = []; + + constructor(private fields: SearchSourceFields = {}) {} + + /** *** + * PUBLIC API + *****/ + + setPreferredSearchStrategyId(searchStrategyId: string) { + this.searchStrategyId = searchStrategyId; + } + + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; + } + + setField(field: K, value: SearchSourceFields[K]) { + if (value == null) { + delete this.fields[field]; + } else { + this.fields[field] = value; + } + return this; + } + + getId() { + return this.id; + } + + getFields() { + return { ...this.fields }; + } + + /** + * Get fields from the fields + */ + getField(field: K, recurse = true): SearchSourceFields[K] { + if (!recurse || this.fields[field] !== void 0) { + return this.fields[field]; + } + const parent = this.getParent(); + return parent && parent.getField(field); + } + + /** + * Get the field from our own fields, don't traverse up the chain + */ + getOwnField(field: K): SearchSourceFields[K] { + return this.getField(field, false); + } + + create() { + return new SearchSource(); + } + + createCopy() { + const newSearchSource = new SearchSource(); + newSearchSource.setFields({ ...this.fields }); + // when serializing the internal fields we lose the internal classes used in the index + // pattern, so we have to set it again to workaround this behavior + newSearchSource.setField('index', this.getField('index')); + newSearchSource.setParent(this.getParent()); + return newSearchSource; + } + + createChild(options = {}) { + const childSearchSource = new SearchSource(); + childSearchSource.setParent(this, options); + return childSearchSource; + } + + /** + * Set a searchSource that this source should inherit from + * @param {SearchSource} parent - the parent searchSource + * @param {SearchSourceOptions} options - the inherit options + * @return {this} - chainable + */ + setParent(parent?: SearchSourceContract, options: SearchSourceOptions = {}) { + this.parent = parent as SearchSource; + this.inheritOptions = options; + return this; + } + + /** + * Get the parent of this SearchSource + * @return {undefined|searchSource} + */ + getParent() { + return this.parent; + } + + /** + * Fetch this source and reject the returned Promise on error + * + * @async + */ + async fetch(options: FetchOptions = {}) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const es = $injector.get('es') as ApiCaller; + + await this.requestIsStarting(options); + + const searchRequest = await this.flatten(); + this.history = [searchRequest]; + + const response = await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { es, config, esShardTimeout } + ); + + if (response.error) { + throw new RequestFailure(null, response); + } + + return response; + } + + /** + * Add a handler that will be notified whenever requests start + * @param {Function} handler + * @return {undefined} + */ + onRequestStart( + handler: (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + ) { + this.requestStartHandlers.push(handler); + } + + async getSearchRequestBody() { + const searchRequest = await this.flatten(); + return searchRequest.body; + } + + /** + * Completely destroy the SearchSource. + * @return {undefined} + */ + destroy() { + this.requestStartHandlers.length = 0; + } + + /** **** + * PRIVATE APIS + ******/ + + /** + * Called by requests of this search source when they are started + * @param {Courier.Request} request + * @param options + * @return {Promise} + */ + private requestIsStarting(options: FetchOptions = {}) { + const handlers = [...this.requestStartHandlers]; + // If callParentStartHandlers has been set to true, we also call all + // handlers of parent search sources. + if (this.inheritOptions.callParentStartHandlers) { + let searchSource = this.getParent(); + while (searchSource) { + handlers.push(...searchSource.requestStartHandlers); + searchSource = searchSource.getParent(); + } + } + + return Promise.all(handlers.map(fn => fn(this, options))); + } + + /** + * Used to merge properties into the data within ._flatten(). + * The data is passed in and modified by the function + * + * @param {object} data - the current merged data + * @param {*} val - the value at `key` + * @param {*} key - The key of `val` + * @return {undefined} + */ + private mergeProp( + data: SearchRequest, + val: SearchSourceFields[K], + key: K + ) { + val = typeof val === 'function' ? val(this) : val; + if (val == null || !key) return; + + const addToRoot = (rootKey: string, value: any) => { + data[rootKey] = value; + }; + + /** + * Add the key and val to the body of the request + */ + const addToBody = (bodyKey: string, value: any) => { + // ignore if we already have a value + if (data.body[bodyKey] == null) { + data.body[bodyKey] = value; + } + }; + + switch (key) { + case 'filter': + return addToRoot('filters', (data.filters || []).concat(val)); + case 'query': + return addToRoot(key, (data[key] || []).concat(val)); + case 'fields': + const fields = _.uniq((data[key] || []).concat(val)); + return addToRoot(key, fields); + case 'index': + case 'type': + case 'highlightAll': + return key && data[key] == null && addToRoot(key, val); + case 'searchAfter': + return addToBody('search_after', val); + case 'source': + return addToBody('_source', val); + case 'sort': + const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + return addToBody(key, sort); + default: + return addToBody(key, val); + } + } + + /** + * Walk the inheritance chain of a source and return its + * flat representation (taking into account merging rules) + * @returns {Promise} + * @resolved {Object|null} - the flat data of the SearchSource + */ + private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { + Object.entries(this.fields).forEach(([key, value]) => { + this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); + }); + if (this.parent) { + this.parent.mergeProps(root, searchRequest); + } + return searchRequest; + } + + private flatten() { + const searchRequest = this.mergeProps(); + + searchRequest.body = searchRequest.body || {}; + const { body, index, fields, query, filters, highlightAll } = searchRequest; + + const computedFields = index ? index.getComputedFields() : {}; + + body.stored_fields = computedFields.storedFields; + body.script_fields = body.script_fields || {}; + _.extend(body.script_fields, computedFields.scriptFields); + + const defaultDocValueFields = computedFields.docvalueFields + ? computedFields.docvalueFields + : []; + body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + + if (!body.hasOwnProperty('_source') && index) { + body._source = index.getSourceFiltering(); + } + + if (body._source) { + // exclude source fields for this index pattern specified by the user + const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => + filter(docvalueField.field) + ); + } + + // if we only want to search for certain fields + if (fields) { + // filter out the docvalue_fields, and script_fields to only include those that we are concerned with + body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); + body.script_fields = _.pick(body.script_fields, fields); + + // request the remaining fields from both stored_fields and _source + const remainingFields = _.difference(fields, _.keys(body.script_fields)); + body.stored_fields = remainingFields; + _.set(body, '_source.includes', remainingFields); + } + + const esQueryConfigs = esQuery.getEsQueryConfig(config); + body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); + + if (highlightAll && body.query) { + body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + delete searchRequest.highlightAll; + } + + const translateToQuery = (filter: esFilters.Filter) => filter && (filter.query || filter); + + // re-write filters within filter aggregations + (function recurse(aggBranch) { + if (!aggBranch) return; + Object.keys(aggBranch).forEach(function(id) { + const agg = aggBranch[id]; + + if (agg.filters) { + // translate filters aggregations + const { filters: aggFilters } = agg.filters; + Object.keys(aggFilters).forEach(filterId => { + aggFilters[filterId] = translateToQuery(aggFilters[filterId]); + }); + } + + recurse(agg.aggs || agg.aggregations); + }); + })(body.aggs || body.aggregations); + + return searchRequest; + } +} diff --git a/src/legacy/ui/public/courier/search_source/types.ts b/src/legacy/ui/public/courier/search_source/types.ts new file mode 100644 index 0000000000000..293f3d49596c3 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/types.ts @@ -0,0 +1,106 @@ +/* + * 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 { NameList } from 'elasticsearch'; +import { esFilters, Query } from '../../../../../plugins/data/public'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +export type EsQuerySearchAfter = [string | number, string | number]; + +export enum SortDirection { + asc = 'asc', + desc = 'desc', +} + +export type EsQuerySortValue = Record; + +export interface SearchSourceFields { + type?: string; + query?: Query; + filter?: + | esFilters.Filter[] + | esFilters.Filter + | (() => esFilters.Filter[] | esFilters.Filter | undefined); + sort?: EsQuerySortValue | EsQuerySortValue[]; + highlight?: any; + highlightAll?: boolean; + aggs?: any; + from?: number; + size?: number; + source?: NameList; + version?: boolean; + fields?: NameList; + index?: IndexPattern; + searchAfter?: EsQuerySearchAfter; +} + +export interface SearchSourceOptions { + callParentStartHandlers?: boolean; +} + +export { SearchSourceContract } from './search_source'; + +export interface SortOptions { + mode?: 'min' | 'max' | 'sum' | 'avg' | 'median'; + type?: 'double' | 'long' | 'date' | 'date_nanos'; + nested?: object; + unmapped_type?: string; + distance_type?: 'arc' | 'plane'; + unit?: string; + ignore_unmapped?: boolean; + _script?: object; +} + +export interface Request { + docvalue_fields: string[]; + _source: unknown; + query: unknown; + script_fields: unknown; + sort: unknown; + stored_fields: string[]; +} + +export interface ResponseWithShardFailure { + _shards: { + failed: number; + failures: ShardFailure[]; + skipped: number; + successful: number; + total: number; + }; +} + +export interface ShardFailure { + index: string; + node: string; + reason: { + caused_by: { + reason: string; + type: string; + }; + reason: string; + lang?: string; + script?: string; + script_stack?: string[]; + type: string; + }; + shard: number; +} + +export type SearchRequest = any; +export type SearchResponse = any; diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js deleted file mode 100644 index 42a9b64136454..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js +++ /dev/null @@ -1,79 +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 { addSearchStrategy } from './search_strategy_registry'; -import { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; -import { getSearchParams, getMSearchParams, getPreference, getTimeout } from '../fetch/get_search_params'; - -export const defaultSearchStrategy = { - id: 'default', - - search: params => { - return params.config.get('courier:batchSearches') ? msearch(params) : search(params); - }, - - isViable: (indexPattern) => { - if (!indexPattern) { - return false; - } - - return isDefaultTypeIndexPattern(indexPattern); - }, -}; - -function msearch({ searchRequests, es, config, esShardTimeout }) { - const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { - const inlineHeader = { - index: index.title || index, - search_type: searchType, - ignore_unavailable: true, - preference: getPreference(config) - }; - const inlineBody = { - ...body, - timeout: getTimeout(esShardTimeout) - }; - return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; - }); - - const searching = es.msearch({ - ...getMSearchParams(config), - body: `${inlineRequests.join('\n')}\n`, - }); - return { - searching: searching.then(({ responses }) => responses), - abort: searching.abort - }; -} - -function search({ searchRequests, es, config, esShardTimeout }) { - const abortController = new AbortController(); - const searchParams = getSearchParams(config, esShardTimeout); - const promises = searchRequests.map(({ index, body }) => { - const searching = es.search({ index: index.title || index, body, ...searchParams }); - abortController.signal.addEventListener('abort', searching.abort); - return searching.catch(({ response }) => JSON.parse(response)); - }); - return { - searching: Promise.all(promises), - abort: () => abortController.abort(), - }; -} - -addSearchStrategy(defaultSearchStrategy); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js deleted file mode 100644 index a1ea53e8b5b47..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js +++ /dev/null @@ -1,106 +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 { defaultSearchStrategy } from './default_search_strategy'; - -const { search } = defaultSearchStrategy; - -function getConfigStub(config = {}) { - return { - get: key => config[key] - }; -} - -const msearchMockResponse = Promise.resolve([]); -msearchMockResponse.abort = jest.fn(); -const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); - -const searchMockResponse = Promise.resolve([]); -searchMockResponse.abort = jest.fn(); -const searchMock = jest.fn().mockReturnValue(searchMockResponse); - -describe('defaultSearchStrategy', function () { - describe('search', function () { - let searchArgs; - - beforeEach(() => { - msearchMockResponse.abort.mockClear(); - msearchMock.mockClear(); - - searchMockResponse.abort.mockClear(); - searchMock.mockClear(); - - searchArgs = { - searchRequests: [{ - index: { title: 'foo' } - }], - es: { - msearch: msearchMock, - search: searchMock, - }, - }; - }); - - test('does not send max_concurrent_shard_requests by default', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); - }); - - test('allows configuration of max_concurrent_shard_requests', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true, - 'courier:maxConcurrentShardRequests': 42, - }); - await search(searchArgs); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); - }); - - test('should set rest_total_hits_as_int to true on a request', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); - }); - - test('should set ignore_throttled=false when including frozen indices', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true, - 'search:includeFrozen': true, - }); - await search(searchArgs); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); - }); - - test('should properly call abort with msearch', () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true - }); - search(searchArgs).abort(); - expect(msearchMockResponse.abort).toHaveBeenCalled(); - }); - - test('should properly abort with search', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': false - }); - search(searchArgs).abort(); - expect(searchMockResponse.abort).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts new file mode 100644 index 0000000000000..29921fc7a11d3 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { defaultSearchStrategy } from './default_search_strategy'; +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchStrategySearchParams } from './types'; + +const { search } = defaultSearchStrategy; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; +} + +const msearchMockResponse: any = Promise.resolve([]); +msearchMockResponse.abort = jest.fn(); +const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); + +const searchMockResponse: any = Promise.resolve([]); +searchMockResponse.abort = jest.fn(); +const searchMock = jest.fn().mockReturnValue(searchMockResponse); + +describe('defaultSearchStrategy', function() { + describe('search', function() { + let searchArgs: MockedKeys>; + + beforeEach(() => { + msearchMockResponse.abort.mockClear(); + msearchMock.mockClear(); + + searchMockResponse.abort.mockClear(); + searchMock.mockClear(); + + searchArgs = { + searchRequests: [ + { + index: { title: 'foo' }, + }, + ], + esShardTimeout: 0, + es: { + msearch: msearchMock, + search: searchMock, + }, + }; + }); + + test('does not send max_concurrent_shard_requests by default', async () => { + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); + expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); + }); + + test('allows configuration of max_concurrent_shard_requests', async () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + 'courier:maxConcurrentShardRequests': 42, + }); + await search({ ...searchArgs, config }); + expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); + }); + + test('should set rest_total_hits_as_int to true on a request', async () => { + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); + expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); + }); + + test('should set ignore_throttled=false when including frozen indices', async () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + 'search:includeFrozen': true, + }); + await search({ ...searchArgs, config }); + expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); + }); + + test('should properly call abort with msearch', () => { + const config = getConfigStub({ + 'courier:batchSearches': true, + }); + search({ ...searchArgs, config }).abort(); + expect(msearchMockResponse.abort).toHaveBeenCalled(); + }); + + test('should properly abort with search', async () => { + const config = getConfigStub({ + 'courier:batchSearches': false, + }); + search({ ...searchArgs, config }).abort(); + expect(searchMockResponse.abort).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts new file mode 100644 index 0000000000000..5be4fef076655 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts @@ -0,0 +1,81 @@ +/* + * 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 { SearchStrategyProvider, SearchStrategySearchParams } from './types'; +import { addSearchStrategy } from './search_strategy_registry'; +import { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; +import { + getSearchParams, + getMSearchParams, + getPreference, + getTimeout, +} from '../fetch/get_search_params'; + +export const defaultSearchStrategy: SearchStrategyProvider = { + id: 'default', + + search: params => { + return params.config.get('courier:batchSearches') ? msearch(params) : search(params); + }, + + isViable: indexPattern => { + return indexPattern && isDefaultTypeIndexPattern(indexPattern); + }, +}; + +function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { + const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { + const inlineHeader = { + index: index.title || index, + search_type: searchType, + ignore_unavailable: true, + preference: getPreference(config), + }; + const inlineBody = { + ...body, + timeout: getTimeout(esShardTimeout), + }; + return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; + }); + + const searching = es.msearch({ + ...getMSearchParams(config), + body: `${inlineRequests.join('\n')}\n`, + }); + return { + searching: searching.then(({ responses }) => responses), + abort: searching.abort, + }; +} + +function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { + const abortController = new AbortController(); + const searchParams = getSearchParams(config, esShardTimeout); + const promises = searchRequests.map(({ index, body }) => { + const searching = es.search({ index: index.title || index, body, ...searchParams }); + abortController.signal.addEventListener('abort', searching.abort); + return searching.catch(({ response }) => JSON.parse(response)); + }); + return { + searching: Promise.all(promises), + abort: () => abortController.abort(), + }; +} + +addSearchStrategy(defaultSearchStrategy); diff --git a/src/legacy/ui/public/courier/search_strategy/index.d.ts b/src/legacy/ui/public/courier/search_strategy/index.d.ts deleted file mode 100644 index dc98484655d00..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/index.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export { SearchError, getSearchErrorType } from './search_error'; diff --git a/src/legacy/ui/public/courier/search_strategy/index.js b/src/legacy/ui/public/courier/search_strategy/index.ts similarity index 100% rename from src/legacy/ui/public/courier/search_strategy/index.js rename to src/legacy/ui/public/courier/search_strategy/index.ts diff --git a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js deleted file mode 100644 index 94c85c0e13ec7..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js +++ /dev/null @@ -1,23 +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. - */ - -export const isDefaultTypeIndexPattern = indexPattern => { - // Default index patterns don't have `type` defined. - return !indexPattern.type; -}; diff --git a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts new file mode 100644 index 0000000000000..3785ce6341078 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts @@ -0,0 +1,25 @@ +/* + * 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 { IndexPattern } from '../../../../core_plugins/data/public'; + +export const isDefaultTypeIndexPattern = (indexPattern: IndexPattern) => { + // Default index patterns don't have `type` defined. + return !indexPattern.type; +}; diff --git a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js deleted file mode 100644 index c4499cc870d56..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js +++ /dev/null @@ -1,49 +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 { SearchError } from './search_error'; -import { i18n } from '@kbn/i18n'; - -export const noOpSearchStrategy = { - id: 'noOp', - - search: async () => { - const searchError = new SearchError({ - status: '418', // "I'm a teapot" error - title: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageTitle', { - defaultMessage: 'No search strategy registered', - }), - message: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', { - defaultMessage: `Couldn't find a search strategy for the search request`, - }), - type: 'NO_OP_SEARCH_STRATEGY', - path: '', - }); - - return { - searching: Promise.reject(searchError), - abort: () => {}, - failedSearchRequests: [], - }; - }, - - isViable: () => { - return true; - }, -}; diff --git a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts new file mode 100644 index 0000000000000..24c3876cfcc05 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.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 { i18n } from '@kbn/i18n'; +import { SearchError } from './search_error'; +import { SearchStrategyProvider } from './types'; + +export const noOpSearchStrategy: SearchStrategyProvider = { + id: 'noOp', + + search: () => { + const searchError = new SearchError({ + status: '418', // "I'm a teapot" error + title: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageTitle', { + defaultMessage: 'No search strategy registered', + }), + message: i18n.translate( + 'common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', + { + defaultMessage: `Couldn't find a search strategy for the search request`, + } + ), + type: 'NO_OP_SEARCH_STRATEGY', + path: '', + }); + + return { + searching: Promise.reject(searchError), + abort: () => {}, + }; + }, + + isViable: () => { + return true; + }, +}; diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts b/src/legacy/ui/public/courier/search_strategy/search_error.d.ts deleted file mode 100644 index bf49853957c75..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts +++ /dev/null @@ -1,21 +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. - */ - -export type SearchError = any; -export type getSearchErrorType = any; diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.js b/src/legacy/ui/public/courier/search_strategy/search_error.js deleted file mode 100644 index 9c35d11a6abf4..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_error.js +++ /dev/null @@ -1,47 +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. - */ - -export class SearchError extends Error { - constructor({ status, title, message, path, type }) { - super(message); - this.name = 'SearchError'; - this.status = status; - this.title = title; - this.message = message; - this.path = path; - this.type = type; - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, SearchError); - } - - // Babel doesn't support traditional `extends` syntax for built-in classes. - // https://babeljs.io/docs/en/caveats/#classes - Object.setPrototypeOf(this, SearchError.prototype); - } -} - -export function getSearchErrorType({ message }) { - const msg = message.toLowerCase(); - if(msg.indexOf('unsupported query') > -1) { - return 'UNSUPPORTED_QUERY'; - } -} diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.ts b/src/legacy/ui/public/courier/search_strategy/search_error.ts new file mode 100644 index 0000000000000..d4042fb17499c --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/search_error.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +interface SearchErrorOptions { + status: string; + title: string; + message: string; + path: string; + type: string; +} + +export class SearchError extends Error { + public name: string; + public status: string; + public title: string; + public message: string; + public path: string; + public type: string; + + constructor({ status, title, message, path, type }: SearchErrorOptions) { + super(message); + this.name = 'SearchError'; + this.status = status; + this.title = title; + this.message = message; + this.path = path; + this.type = type; + + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, SearchError); + } + + // Babel doesn't support traditional `extends` syntax for built-in classes. + // https://babeljs.io/docs/en/caveats/#classes + Object.setPrototypeOf(this, SearchError.prototype); + } +} + +export function getSearchErrorType({ message }: Pick) { + const msg = message.toLowerCase(); + if (msg.indexOf('unsupported query') > -1) { + return 'UNSUPPORTED_QUERY'; + } +} diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js deleted file mode 100644 index e67d39ea27aa6..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js +++ /dev/null @@ -1,63 +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 { noOpSearchStrategy } from './no_op_search_strategy'; - -export const searchStrategies = []; - -export const addSearchStrategy = searchStrategy => { - if (searchStrategies.includes(searchStrategy)) { - return; - } - - searchStrategies.push(searchStrategy); -}; - -export const getSearchStrategyByViability = indexPattern => { - return searchStrategies.find(searchStrategy => { - return searchStrategy.isViable(indexPattern); - }); -}; - -export const getSearchStrategyById = searchStrategyId => { - return searchStrategies.find(searchStrategy => { - return searchStrategy.id === searchStrategyId; - }); -}; - -export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrategyId } = {}) => { - // Allow the searchSource to declare the correct strategy with which to execute its searches. - if (searchStrategyId != null) { - return getSearchStrategyById(searchStrategyId); - } - - // Otherwise try to match it to a strategy. - const viableSearchStrategy = getSearchStrategyByViability(searchRequest.index); - - if (viableSearchStrategy) { - return viableSearchStrategy; - } - - // This search strategy automatically rejects with an error. - return noOpSearchStrategy; -}; - -export const hasSearchStategyForIndexPattern = indexPattern => { - return Boolean(getSearchStrategyByViability(indexPattern)); -}; diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js deleted file mode 100644 index 362d303eb6203..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js +++ /dev/null @@ -1,114 +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 { noOpSearchStrategy } from './no_op_search_strategy'; -import { - searchStrategies, - addSearchStrategy, - getSearchStrategyByViability, - getSearchStrategyById, - getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern -} from './search_strategy_registry'; - -const mockSearchStrategies = [{ - id: 0, - isViable: index => index === 0 -}, { - id: 1, - isViable: index => index === 1 -}]; - -describe('Search strategy registry', () => { - beforeEach(() => { - searchStrategies.length = 0; - }); - - describe('addSearchStrategy', () => { - it('adds a search strategy', () => { - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - - it('does not add a search strategy if it is already included', () => { - addSearchStrategy(mockSearchStrategies[0]); - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - }); - - describe('getSearchStrategyByViability', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the viable strategy', () => { - expect(getSearchStrategyByViability(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyByViability(1)).toBe(mockSearchStrategies[1]); - }); - - it('returns undefined if there is no viable strategy', () => { - expect(getSearchStrategyByViability(-1)).toBe(undefined); - }); - }); - - describe('getSearchStrategyById', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID', () => { - expect(getSearchStrategyById(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById(1)).toBe(mockSearchStrategies[1]); - }); - - it('returns undefined if there is no strategy with that ID', () => { - expect(getSearchStrategyById(-1)).toBe(undefined); - }); - }); - - describe('getSearchStrategyForSearchRequest', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: 1 })).toBe(mockSearchStrategies[1]); - }); - - it('returns the strategy by viability if there is one', () => { - expect(getSearchStrategyForSearchRequest({ index: 1 })).toBe(mockSearchStrategies[1]); - }); - - it('returns the no op strategy if there is no viable strategy', () => { - expect(getSearchStrategyForSearchRequest({ index: 3 })).toBe(noOpSearchStrategy); - }); - }); - - describe('hasSearchStategyForIndexPattern', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern(0)).toBe(true); - expect(hasSearchStategyForIndexPattern(-1)).toBe(false); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts new file mode 100644 index 0000000000000..ae2ed6128c8ea --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { IndexPattern } from '../../../../core_plugins/data/public'; +import { noOpSearchStrategy } from './no_op_search_strategy'; +import { + searchStrategies, + addSearchStrategy, + getSearchStrategyByViability, + getSearchStrategyById, + getSearchStrategyForSearchRequest, + hasSearchStategyForIndexPattern, +} from './search_strategy_registry'; +import { SearchStrategyProvider } from './types'; + +const mockSearchStrategies: SearchStrategyProvider[] = [ + { + id: '0', + isViable: (index: IndexPattern) => index.id === '0', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, + { + id: '1', + isViable: (index: IndexPattern) => index.id === '1', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, +]; + +describe('Search strategy registry', () => { + beforeEach(() => { + searchStrategies.length = 0; + }); + + describe('addSearchStrategy', () => { + it('adds a search strategy', () => { + addSearchStrategy(mockSearchStrategies[0]); + expect(searchStrategies.length).toBe(1); + }); + + it('does not add a search strategy if it is already included', () => { + addSearchStrategy(mockSearchStrategies[0]); + addSearchStrategy(mockSearchStrategies[0]); + expect(searchStrategies.length).toBe(1); + }); + }); + + describe('getSearchStrategyByViability', () => { + beforeEach(() => { + mockSearchStrategies.forEach(addSearchStrategy); + }); + + it('returns the viable strategy', () => { + expect(getSearchStrategyByViability({ id: '0' } as IndexPattern)).toBe( + mockSearchStrategies[0] + ); + expect(getSearchStrategyByViability({ id: '1' } as IndexPattern)).toBe( + mockSearchStrategies[1] + ); + }); + + it('returns undefined if there is no viable strategy', () => { + expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); + }); + }); + + describe('getSearchStrategyById', () => { + beforeEach(() => { + mockSearchStrategies.forEach(addSearchStrategy); + }); + + it('returns the strategy by ID', () => { + expect(getSearchStrategyById('0')).toBe(mockSearchStrategies[0]); + expect(getSearchStrategyById('1')).toBe(mockSearchStrategies[1]); + }); + + it('returns undefined if there is no strategy with that ID', () => { + expect(getSearchStrategyById('-1')).toBe(undefined); + }); + + it('returns the noOp search strategy if passed that ID', () => { + expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); + }); + }); + + describe('getSearchStrategyForSearchRequest', () => { + beforeEach(() => { + mockSearchStrategies.forEach(addSearchStrategy); + }); + + it('returns the strategy by ID if provided', () => { + expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: '1' })).toBe( + mockSearchStrategies[1] + ); + }); + + it('throws if there is no strategy by provided ID', () => { + expect(() => + getSearchStrategyForSearchRequest({}, { searchStrategyId: '-1' }) + ).toThrowErrorMatchingInlineSnapshot(`"No strategy with ID -1"`); + }); + + it('returns the strategy by viability if there is one', () => { + expect( + getSearchStrategyForSearchRequest({ + index: { + id: '1', + }, + }) + ).toBe(mockSearchStrategies[1]); + }); + + it('returns the no op strategy if there is no viable strategy', () => { + expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); + }); + }); + + describe('hasSearchStategyForIndexPattern', () => { + beforeEach(() => { + mockSearchStrategies.forEach(addSearchStrategy); + }); + + it('returns whether there is a search strategy for this index pattern', () => { + expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); + expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts new file mode 100644 index 0000000000000..9ef007f97531e --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { SearchStrategyProvider } from './types'; +import { noOpSearchStrategy } from './no_op_search_strategy'; +import { SearchResponse } from '../types'; + +export const searchStrategies: SearchStrategyProvider[] = []; + +export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { + if (searchStrategies.includes(searchStrategy)) { + return; + } + + searchStrategies.push(searchStrategy); +}; + +export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { + return searchStrategies.find(searchStrategy => { + return searchStrategy.isViable(indexPattern); + }); +}; + +export const getSearchStrategyById = (searchStrategyId: string) => { + return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { + return searchStrategy.id === searchStrategyId; + }); +}; + +export const getSearchStrategyForSearchRequest = ( + searchRequest: SearchResponse, + { searchStrategyId }: { searchStrategyId?: string } = {} +) => { + // Allow the searchSource to declare the correct strategy with which to execute its searches. + if (searchStrategyId != null) { + const strategy = getSearchStrategyById(searchStrategyId); + if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); + return strategy; + } + + // Otherwise try to match it to a strategy. + const viableSearchStrategy = getSearchStrategyByViability(searchRequest.index); + + if (viableSearchStrategy) { + return viableSearchStrategy; + } + + // This search strategy automatically rejects with an error. + return noOpSearchStrategy; +}; + +export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { + return Boolean(getSearchStrategyByViability(indexPattern)); +}; diff --git a/src/legacy/ui/public/courier/search_strategy/types.ts b/src/legacy/ui/public/courier/search_strategy/types.ts new file mode 100644 index 0000000000000..1542f9824a5b1 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { IndexPattern } from '../../../../core_plugins/data/public'; +import { FetchHandlers } from '../fetch/types'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface SearchStrategyProvider { + id: string; + search: (params: SearchStrategySearchParams) => SearchStrategyResponse; + isViable: (indexPattern: IndexPattern) => boolean; +} + +export interface SearchStrategyResponse { + searching: Promise; + abort: () => void; +} + +export interface SearchStrategySearchParams extends FetchHandlers { + searchRequests: SearchRequest[]; +} diff --git a/src/legacy/ui/public/courier/types.ts b/src/legacy/ui/public/courier/types.ts new file mode 100644 index 0000000000000..23d74ce6a57da --- /dev/null +++ b/src/legacy/ui/public/courier/types.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './fetch/types'; +export * from './search_source/types'; +export * from './search_strategy/types'; +export * from './utils/types'; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts b/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts deleted file mode 100644 index 7f638d357a9e1..0000000000000 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts +++ /dev/null @@ -1,52 +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 { SearchSource } from 'ui/courier'; - -interface InspectorStat { - label: string; - value: string; - description: string; -} - -interface RequestInspectorStats { - indexPattern: InspectorStat; - indexPatternId: InspectorStat; -} - -interface ResponseInspectorStats { - queryTime: InspectorStat; - hitsTotal: InspectorStat; - hits: InspectorStat; - requestTime: InspectorStat; -} - -interface Response { - took: number; - hits: { - total: number; - hits: any[]; - }; -} - -export function getRequestInspectorStats(searchSource: SearchSource): RequestInspectorStats; -export function getResponseInspectorStats( - searchSource: SearchSource, - resp: Response -): ResponseInspectorStats; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js b/src/legacy/ui/public/courier/utils/courier_inspector_utils.js deleted file mode 100644 index 0e53f92bd9dcb..0000000000000 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js +++ /dev/null @@ -1,118 +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. - */ - -/** - * This function collects statistics from a SearchSource and a response - * for the usage in the inspector stats panel. Pass in a searchSource and a response - * and the returned object can be passed to the `stats` method of the request - * logger. - */ - -import { i18n } from '@kbn/i18n'; - -function getRequestInspectorStats(searchSource) { - const stats = {}; - const index = searchSource.getField('index'); - - if (index) { - stats.indexPattern = { - label: i18n.translate('common.ui.courier.indexPatternLabel', { - defaultMessage: 'Index pattern' - }), - value: index.title, - description: i18n.translate('common.ui.courier.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.' - }), - }; - stats.indexPatternId = { - label: i18n.translate('common.ui.courier.indexPatternIdLabel', { - defaultMessage: 'Index pattern ID' - }), - value: index.id, - description: i18n.translate('common.ui.courier.indexPatternIdDescription', { - defaultMessage: 'The ID in the {kibanaIndexPattern} index.', - values: { kibanaIndexPattern: '.kibana' } - }), - }; - } - - return stats; -} -function getResponseInspectorStats(searchSource, resp) { - const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; - const stats = {}; - - if (resp && resp.took) { - stats.queryTime = { - label: i18n.translate('common.ui.courier.queryTimeLabel', { - defaultMessage: 'Query time' - }), - value: i18n.translate('common.ui.courier.queryTimeValue', { - defaultMessage: '{queryTime}ms', - values: { queryTime: resp.took }, - }), - description: i18n.translate('common.ui.courier.queryTimeDescription', { - defaultMessage: 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.' - }), - }; - } - - if (resp && resp.hits) { - stats.hitsTotal = { - label: i18n.translate('common.ui.courier.hitsTotalLabel', { - defaultMessage: 'Hits (total)' - }), - value: `${resp.hits.total}`, - description: i18n.translate('common.ui.courier.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.' - }), - }; - - stats.hits = { - label: i18n.translate('common.ui.courier.hitsLabel', { - defaultMessage: 'Hits' - }), - value: `${resp.hits.hits.length}`, - description: i18n.translate('common.ui.courier.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.' - }), - }; - } - - if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) { - stats.requestTime = { - label: i18n.translate('common.ui.courier.requestTimeLabel', { - defaultMessage: 'Request time' - }), - value: i18n.translate('common.ui.courier.requestTimeValue', { - defaultMessage: '{requestTime}ms', - values: { requestTime: lastRequest.ms }, - }), - description: i18n.translate('common.ui.courier.requestTimeDescription', { - defaultMessage: 'The time of the request from the browser to Elasticsearch and back. ' + - 'Does not include the time the requested waited in the queue.' - }), - }; - } - - return stats; -} - -export { getRequestInspectorStats, getResponseInspectorStats }; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts new file mode 100644 index 0000000000000..2c47fae4cce37 --- /dev/null +++ b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts @@ -0,0 +1,125 @@ +/* + * 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. + */ + +/** + * This function collects statistics from a SearchSource and a response + * for the usage in the inspector stats panel. Pass in a searchSource and a response + * and the returned object can be passed to the `stats` method of the request + * logger. + */ + +import { i18n } from '@kbn/i18n'; +import { SearchResponse } from 'elasticsearch'; +import { SearchSourceContract, RequestInspectorStats } from '../types'; + +function getRequestInspectorStats(searchSource: SearchSourceContract) { + const stats: RequestInspectorStats = {}; + const index = searchSource.getField('index'); + + if (index) { + stats.indexPattern = { + label: i18n.translate('common.ui.courier.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: index.title, + description: i18n.translate('common.ui.courier.indexPatternDescription', { + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', + }), + }; + stats.indexPatternId = { + label: i18n.translate('common.ui.courier.indexPatternIdLabel', { + defaultMessage: 'Index pattern ID', + }), + value: index.id!, + description: i18n.translate('common.ui.courier.indexPatternIdDescription', { + defaultMessage: 'The ID in the {kibanaIndexPattern} index.', + values: { kibanaIndexPattern: '.kibana' }, + }), + }; + } + + return stats; +} +function getResponseInspectorStats( + searchSource: SearchSourceContract, + resp: SearchResponse +) { + const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; + const stats: RequestInspectorStats = {}; + + if (resp && resp.took) { + stats.queryTime = { + label: i18n.translate('common.ui.courier.queryTimeLabel', { + defaultMessage: 'Query time', + }), + value: i18n.translate('common.ui.courier.queryTimeValue', { + defaultMessage: '{queryTime}ms', + values: { queryTime: resp.took }, + }), + description: i18n.translate('common.ui.courier.queryTimeDescription', { + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', + }), + }; + } + + if (resp && resp.hits) { + stats.hitsTotal = { + label: i18n.translate('common.ui.courier.hitsTotalLabel', { + defaultMessage: 'Hits (total)', + }), + value: `${resp.hits.total}`, + description: i18n.translate('common.ui.courier.hitsTotalDescription', { + defaultMessage: 'The number of documents that match the query.', + }), + }; + + stats.hits = { + label: i18n.translate('common.ui.courier.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${resp.hits.hits.length}`, + description: i18n.translate('common.ui.courier.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }; + } + + if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) { + stats.requestTime = { + label: i18n.translate('common.ui.courier.requestTimeLabel', { + defaultMessage: 'Request time', + }), + value: i18n.translate('common.ui.courier.requestTimeValue', { + defaultMessage: '{requestTime}ms', + values: { requestTime: lastRequest.ms }, + }), + description: i18n.translate('common.ui.courier.requestTimeDescription', { + defaultMessage: + 'The time of the request from the browser to Elasticsearch and back. ' + + 'Does not include the time the requested waited in the queue.', + }), + }; + } + + return stats; +} + +export { getRequestInspectorStats, getResponseInspectorStats }; diff --git a/src/legacy/ui/public/courier/utils/types.ts b/src/legacy/ui/public/courier/utils/types.ts new file mode 100644 index 0000000000000..305f27a86b398 --- /dev/null +++ b/src/legacy/ui/public/courier/utils/types.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface InspectorStat { + label: string; + value: string; + description: string; +} + +export interface RequestInspectorStats { + indexPattern?: InspectorStat; + indexPatternId?: InspectorStat; + queryTime?: InspectorStat; + hitsTotal?: InspectorStat; + hits?: InspectorStat; + requestTime?: InspectorStat; +} diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index 896fb8fc5ddd8..f3c5990caae64 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -27,10 +27,6 @@ import { getSupportedScriptingLanguages, } from 'ui/scripting_languages'; -import { - fieldFormats -} from 'ui/registry/field_formats'; - import { getDocLink } from 'ui/documentation_links'; @@ -39,6 +35,8 @@ import { toastNotifications } from 'ui/notify'; +import { npStart } from 'ui/new_platform'; + import { EuiBasicTable, EuiButton, @@ -84,7 +82,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; // This loads Ace editor's "groovy" mode, used below to highlight the script. import 'brace/mode/groovy'; +const getFieldFormats = () => npStart.plugins.data.fieldFormats; + export class FieldEditor extends PureComponent { + static propTypes = { indexPattern: PropTypes.object.isRequired, field: PropTypes.object.isRequired, @@ -141,9 +142,10 @@ export class FieldEditor extends PureComponent { const fieldTypes = get(FIELD_TYPES_BY_LANG, field.lang, DEFAULT_FIELD_TYPES); field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; - const DefaultFieldFormat = fieldFormats.getDefaultType(field.type, field.esTypes); + const fieldFormats = getFieldFormats(); + const fieldTypeFormats = [ - getDefaultFormat(DefaultFieldFormat), + getDefaultFormat(fieldFormats.getDefaultType(field.type, field.esTypes)), ...fieldFormats.getByFieldType(field.type), ]; @@ -169,12 +171,14 @@ export class FieldEditor extends PureComponent { onTypeChange = (type) => { const { getConfig } = this.props.helpers; const { field } = this.state; + const fieldFormats = getFieldFormats(); const DefaultFieldFormat = fieldFormats.getDefaultType(type); + field.type = type; const fieldTypeFormats = [ getDefaultFormat(DefaultFieldFormat), - ...fieldFormats.getByFieldType(field.type), + ...getFieldFormats().getByFieldType(field.type), ]; const FieldFormat = fieldTypeFormats[0]; @@ -202,6 +206,7 @@ export class FieldEditor extends PureComponent { const { getConfig } = this.props.helpers; const { field, fieldTypeFormats } = this.state; const FieldFormat = fieldTypeFormats.find((format) => format.id === formatId) || fieldTypeFormats[0]; + field.format = new FieldFormat(params, getConfig); this.setState({ @@ -684,6 +689,7 @@ export class FieldEditor extends PureComponent { } saveField = async () => { + const fieldFormat = this.state.field.format; const field = this.state.field.toActualField(); const { indexPattern } = this.props; const { fieldFormatId } = this.state; @@ -721,7 +727,7 @@ export class FieldEditor extends PureComponent { if (!fieldFormatId) { indexPattern.fieldFormatMap[field.name] = undefined; } else { - indexPattern.fieldFormatMap[field.name] = field.format; + indexPattern.fieldFormatMap[field.name] = fieldFormat; } return indexPattern.save() diff --git a/src/legacy/ui/public/field_editor/field_editor.test.js b/src/legacy/ui/public/field_editor/field_editor.test.js index 34503238f437d..72eebee960b52 100644 --- a/src/legacy/ui/public/field_editor/field_editor.test.js +++ b/src/legacy/ui/public/field_editor/field_editor.test.js @@ -20,9 +20,12 @@ jest.mock('ui/kfetch', () => ({})); import React from 'react'; + +import { npStart } from 'ui/new_platform'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; jest.mock('brace/mode/groovy', () => ({})); +jest.mock('ui/new_platform'); import { FieldEditor } from './field_editor'; @@ -46,6 +49,10 @@ jest.mock('@elastic/eui', () => ({ EuiSpacer: 'eui-spacer', EuiText: 'eui-text', EuiTextArea: 'eui-textArea', + htmlIdGenerator: () => 42, + palettes: { + euiPaletteColorBlind: { colors: ['red'] } + } })); jest.mock('ui/scripting_languages', () => ({ @@ -54,26 +61,6 @@ jest.mock('ui/scripting_languages', () => ({ getDeprecatedScriptingLanguages: () => ['testlang'], })); -jest.mock('ui/registry/field_formats', () => { - class Format { - static id = 'test_format'; static title = 'Test format'; - params() {} - } - - return { - fieldFormats: { - getDefaultType: () => { - return Format; - }, - getByFieldType: (fieldType) => { - if(fieldType === 'number') { - return [Format]; - } - } - }, - }; -}); - jest.mock('ui/documentation_links', () => ({ getDocLink: (doc) => `(docLink for ${doc})`, })); @@ -133,6 +120,13 @@ describe('FieldEditor', () => { indexPattern = { fields, }; + + npStart.plugins.data.fieldFormats.getDefaultType = jest.fn(() => Format); + npStart.plugins.data.fieldFormats.getByFieldType = jest.fn((fieldType) => { + if(fieldType === 'number') { + return [Format]; + } + }); }); it('should render create new scripted field correctly', async () => { diff --git a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js b/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js deleted file mode 100644 index a15c602b7ba83..0000000000000 --- a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js +++ /dev/null @@ -1,126 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { fieldWildcardFilter, makeRegEx } from '../../field_wildcard'; - -describe('fieldWildcard', function () { - const metaFields = ['_id', '_type', '_source']; - - beforeEach(ngMock.module('kibana')); - - describe('makeRegEx', function () { - it('matches * in any position', function () { - expect('aaaaaabbbbbbbcccccc').to.match(makeRegEx('*a*b*c*')); - expect('a1234').to.match(makeRegEx('*1234')); - expect('1234a').to.match(makeRegEx('1234*')); - expect('12a34').to.match(makeRegEx('12a34')); - }); - - it('properly escapes regexp control characters', function () { - expect('account[user_id]').to.match(makeRegEx('account[*]')); - }); - - it('properly limits matches without wildcards', function () { - expect('username').to.match(makeRegEx('*name')); - expect('username').to.match(makeRegEx('user*')); - expect('username').to.match(makeRegEx('username')); - expect('username').to.not.match(makeRegEx('user')); - expect('username').to.not.match(makeRegEx('name')); - expect('username').to.not.match(makeRegEx('erna')); - }); - }); - - describe('filter', function () { - it('filters nothing when given undefined', function () { - const filter = fieldWildcardFilter(); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('filters nothing when given an empty array', function () { - const filter = fieldWildcardFilter([], metaFields); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('does not filter metaFields', function () { - const filter = fieldWildcardFilter([ '_*' ], metaFields); - - const original = [ - '_id', - '_type', - '_typefake' - ]; - - expect(original.filter(filter)).to.eql(['_id', '_type']); - }); - - it('filters values that match the globs', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4' - ], metaFields); - - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(['bar', 'baz']); - }); - - it('handles weird values okay', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4', - 'undefined' - ], metaFields); - - const original = [ - 'foo', - null, - 'bar', - undefined, - {}, - [], - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql([null, 'bar', {}, [], 'baz']); - }); - }); -}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.js b/src/legacy/ui/public/field_wildcard/field_wildcard.js deleted file mode 100644 index 656641b20a98c..0000000000000 --- a/src/legacy/ui/public/field_wildcard/field_wildcard.js +++ /dev/null @@ -1,43 +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 { escapeRegExp, memoize } from 'lodash'; - -export const makeRegEx = memoize(function makeRegEx(glob) { - return new RegExp('^' + glob.split('*').map(escapeRegExp).join('.*') + '$'); -}); - -// Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardMatcher(globs = [], metaFields) { - return function matcher(val) { - // do not test metaFields or keyword - if (metaFields.indexOf(val) !== -1) { - return false; - } - return globs.some(p => makeRegEx(p).test(val)); - }; -} - -// Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardFilter(globs = [], metaFields = []) { - const matcher = fieldWildcardMatcher(globs, metaFields); - return function filter(val) { - return !matcher(val); - }; -} diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts new file mode 100644 index 0000000000000..9f7523866fdc1 --- /dev/null +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { fieldWildcardFilter, makeRegEx } from './field_wildcard'; + +describe('fieldWildcard', () => { + const metaFields = ['_id', '_type', '_source']; + + describe('makeRegEx', function() { + it('matches * in any position', function() { + expect('aaaaaabbbbbbbcccccc').toMatch(makeRegEx('*a*b*c*')); + expect('a1234').toMatch(makeRegEx('*1234')); + expect('1234a').toMatch(makeRegEx('1234*')); + expect('12a34').toMatch(makeRegEx('12a34')); + }); + + it('properly escapes regexp control characters', function() { + expect('account[user_id]').toMatch(makeRegEx('account[*]')); + }); + + it('properly limits matches without wildcards', function() { + expect('username').toMatch(makeRegEx('*name')); + expect('username').toMatch(makeRegEx('user*')); + expect('username').toMatch(makeRegEx('username')); + expect('username').not.toMatch(makeRegEx('user')); + expect('username').not.toMatch(makeRegEx('name')); + expect('username').not.toMatch(makeRegEx('erna')); + }); + }); + + describe('filter', function() { + it('filters nothing when given undefined', function() { + const filter = fieldWildcardFilter(); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(val => filter(val))).toEqual(original); + }); + + it('filters nothing when given an empty array', function() { + const filter = fieldWildcardFilter([], metaFields); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(original); + }); + + it('does not filter metaFields', function() { + const filter = fieldWildcardFilter(['_*'], metaFields); + + const original = ['_id', '_type', '_typefake']; + + expect(original.filter(filter)).toEqual(['_id', '_type']); + }); + + it('filters values that match the globs', function() { + const filter = fieldWildcardFilter(['f*', '*4'], metaFields); + + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(['bar', 'baz']); + }); + + it('handles weird values okay', function() { + const filter = fieldWildcardFilter(['f*', '*4', 'undefined'], metaFields); + + const original = ['foo', null, 'bar', undefined, {}, [], 'baz', 1234]; + + expect(original.filter(filter)).toEqual([null, 'bar', {}, [], 'baz']); + }); + }); +}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.ts b/src/legacy/ui/public/field_wildcard/field_wildcard.ts new file mode 100644 index 0000000000000..5437086ddd6f4 --- /dev/null +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.ts @@ -0,0 +1,47 @@ +/* + * 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 { escapeRegExp, memoize } from 'lodash'; + +export const makeRegEx = memoize(function makeRegEx(glob: string) { + const globRegex = glob + .split('*') + .map(escapeRegExp) + .join('.*'); + return new RegExp(`^${globRegex}$`); +}); + +// Note that this will return an essentially noop function if globs is undefined. +export function fieldWildcardMatcher(globs: string[] = [], metaFields: unknown[] = []) { + return function matcher(val: unknown) { + // do not test metaFields or keyword + if (metaFields.indexOf(val) !== -1) { + return false; + } + return globs.some(p => makeRegEx(p).test(`${val}`)); + }; +} + +// Note that this will return an essentially noop function if globs is undefined. +export function fieldWildcardFilter(globs: string[] = [], metaFields: string[] = []) { + const matcher = fieldWildcardMatcher(globs, metaFields); + return function filter(val: unknown) { + return !matcher(val); + }; +} diff --git a/src/legacy/ui/public/field_wildcard/index.js b/src/legacy/ui/public/field_wildcard/index.ts similarity index 100% rename from src/legacy/ui/public/field_wildcard/index.js rename to src/legacy/ui/public/field_wildcard/index.ts diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index 3fd4137cab625..4d0f5d3a5bd56 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -27,7 +27,7 @@ import { npStart } from 'ui/new_platform'; export const I18nContext = npStart.core.i18n.Context; export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { - const ContextWrapper: React.SFC

= props => { + const ContextWrapper: React.FC

= props => { return ( diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index 2dd3f370c6d6a..145045a90ade8 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -35,8 +35,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - isFilterable, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index 3b4952ac81519..d0ff0aaa8c72c 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -30,7 +30,6 @@ export const { FieldList, // only used in Discover and StubIndexPattern flattenHitWrapper, formatHitProvider, - IndexPatternSelect, // only used in x-pack/plugin/maps and input control vis } = data.indexPatterns; // static code @@ -38,7 +37,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - isFilterable, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index 9c4cee6b05db0..a1d48caf3f489 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -75,8 +75,7 @@ export function createTopNavDirective() { module.directive('kbnTopNav', createTopNavDirective); -export function createTopNavHelper(reactDirective) { - const { TopNavMenu } = navigation.ui; +export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => { return reactDirective( wrapInI18nContext(TopNavMenu), [ @@ -116,6 +115,6 @@ export function createTopNavHelper(reactDirective) { 'showAutoRefreshOnly', ], ); -} +}; -module.directive('kbnTopNavHelper', createTopNavHelper); +module.directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 788718e848430..6e9f5c85aa1b2 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -28,7 +28,7 @@ import { IRootScopeService, } from 'angular'; import $ from 'jquery'; -import { cloneDeep, forOwn, set } from 'lodash'; +import _, { cloneDeep, forOwn, get, set } from 'lodash'; import React, { Fragment } from 'react'; import * as Rx from 'rxjs'; @@ -37,27 +37,43 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, LegacyCoreStart } from 'kibana/public'; import { fatalError } from 'ui/notify'; -import { capabilities } from 'ui/capabilities'; +import { RouteConfiguration } from 'ui/routes/route_manager'; // @ts-ignore import { modifyUrl } from 'ui/url'; import { toMountPoint } from '../../../../plugins/kibana_react/public'; // @ts-ignore import { UrlOverflowService } from '../error_url_overflow'; -import { npStart } from '../new_platform'; -import { toastNotifications } from '../notify'; // @ts-ignore import { isSystemApiRequest } from '../system_api'; const URL_LIMIT_WARN_WITHIN = 1000; -function isDummyWrapperRoute($route: any) { +/** + * Detects whether a given angular route is a dummy route that doesn't + * require any action. There are two ways this can happen: + * If `outerAngularWrapperRoute` is set on the route config object, + * it means the local application service set up this route on the outer angular + * and the internal routes will handle the hooks. + * + * If angular did not detect a route and it is the local angular, we are currently + * navigating away from a URL controlled by a local angular router and the + * application will get unmounted. In this case the outer router will handle + * the hooks. + * @param $route Injected $route dependency + * @param isLocalAngular Flag whether this is the local angular router + */ +function isDummyRoute($route: any, isLocalAngular: boolean) { return ( - $route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute + ($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) || + (!$route.current && isLocalAngular) ); } -export const configureAppAngularModule = (angularModule: IModule) => { - const newPlatform = npStart.core; +export const configureAppAngularModule = ( + angularModule: IModule, + newPlatform: LegacyCoreStart, + isLocalAngular: boolean +) => { const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { @@ -73,15 +89,16 @@ export const configureAppAngularModule = (angularModule: IModule) => { .value('buildSha', legacyMetadata.buildSha) .value('serverName', legacyMetadata.serverName) .value('esUrl', getEsUrl(newPlatform)) - .value('uiCapabilities', capabilities.get()) + .value('uiCapabilities', newPlatform.application.capabilities) .config(setupCompileProvider(newPlatform)) .config(setupLocationProvider(newPlatform)) .config($setupXsrfRequestInterceptor(newPlatform)) .run(capture$httpLoadingCount(newPlatform)) - .run($setupBreadcrumbsAutoClear(newPlatform)) - .run($setupBadgeAutoClear(newPlatform)) - .run($setupHelpExtensionAutoClear(newPlatform)) - .run($setupUrlOverflowHandling(newPlatform)); + .run($setupBreadcrumbsAutoClear(newPlatform, isLocalAngular)) + .run($setupBadgeAutoClear(newPlatform, isLocalAngular)) + .run($setupHelpExtensionAutoClear(newPlatform, isLocalAngular)) + .run($setupUrlOverflowHandling(newPlatform, isLocalAngular)) + .run($setupUICapabilityRedirect(newPlatform)); }; const getEsUrl = (newPlatform: CoreStart) => { @@ -168,12 +185,42 @@ const capture$httpLoadingCount = (newPlatform: CoreStart) => ( ); }; +/** + * integrates with angular to automatically redirect to home if required + * capability is not met + */ +const $setupUICapabilityRedirect = (newPlatform: CoreStart) => ( + $rootScope: IRootScopeService, + $injector: any +) => { + const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); + // this feature only works within kibana app for now after everything is + // switched to the application service, this can be changed to handle all + // apps. + if (!isKibanaAppRoute) { + return; + } + $rootScope.$on( + '$routeChangeStart', + (event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => { + if (!route || !route.requireUICapability) { + return; + } + + if (!get(newPlatform.application.capabilities, route.requireUICapability)) { + $injector.get('kbnUrl').change('/home'); + event.preventDefault(); + } + } + ); +}; + /** * internal angular run function that will be called when angular bootstraps and * lets us integrate with the angular router so that we can automatically clear * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly */ -const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( +const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -195,7 +242,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( }); $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyWrapperRoute($route)) { + if (isDummyRoute($route, isLocalAngular)) { return; } const current = $route.current || {}; @@ -223,7 +270,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( * lets us integrate with the angular router so that we can automatically clear * the badge if we switch to a Kibana app that does not use the badge correctly */ -const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( +const $setupBadgeAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -237,7 +284,7 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( }); $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyWrapperRoute($route)) { + if (isDummyRoute($route, isLocalAngular)) { return; } const current = $route.current || {}; @@ -266,7 +313,7 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( * the helpExtension if we switch to a Kibana app that does not set its own * helpExtension */ -const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( +const $setupHelpExtensionAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -284,14 +331,14 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $route = $injector.has('$route') ? $injector.get('$route') : {}; $rootScope.$on('$routeChangeStart', () => { - if (isDummyWrapperRoute($route)) { + if (isDummyRoute($route, isLocalAngular)) { return; } helpExtensionSetSinceRouteChange = false; }); $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyWrapperRoute($route)) { + if (isDummyRoute($route, isLocalAngular)) { return; } const current = $route.current || {}; @@ -304,7 +351,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( }); }; -const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( +const $setupUrlOverflowHandling = (newPlatform: CoreStart, isLocalAngular: boolean) => ( $location: ILocationService, $rootScope: IRootScopeService, $injector: auto.IInjectorService @@ -312,7 +359,7 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( const $route = $injector.has('$route') ? $injector.get('$route') : {}; const urlOverflow = new UrlOverflowService(); const check = () => { - if (isDummyWrapperRoute($route)) { + if (isDummyRoute($route, isLocalAngular)) { return; } // disable long url checks when storing state in session storage @@ -326,7 +373,7 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( try { if (urlOverflow.check($location.absUrl()) <= URL_LIMIT_WARN_WITHIN) { - toastNotifications.addWarning({ + newPlatform.notifications.toasts.addWarning({ title: i18n.translate('common.ui.chrome.bigUrlWarningNotificationTitle', { defaultMessage: 'The URL is big and Kibana might stop working', }), diff --git a/src/legacy/ui/public/legacy_compat/ensure_default_index_pattern.tsx b/src/legacy/ui/public/legacy_compat/ensure_default_index_pattern.tsx new file mode 100644 index 0000000000000..98e95865d7325 --- /dev/null +++ b/src/legacy/ui/public/legacy_compat/ensure_default_index_pattern.tsx @@ -0,0 +1,105 @@ +/* + * 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 { contains } from 'lodash'; +import { IRootScopeService } from 'angular'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { EuiCallOut } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { DataStart } from '../../../core_plugins/data/public'; + +let bannerId: string; +let timeoutId: NodeJS.Timeout | undefined; + +/** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + * + * If there are no index patterns, redirect to management page and show + * banner. In this case the promise returned from this function will never + * resolve to wait for the URL change to happen. + */ +export async function ensureDefaultIndexPattern( + newPlatform: CoreStart, + data: DataStart, + $rootScope: IRootScopeService, + kbnUrl: any +) { + const patterns = await data.indexPatterns.indexPatterns.getIds(); + let defaultId = newPlatform.uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + newPlatform.uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + newPlatform.uiSettings.set('defaultIndex', defaultId); + } else { + const canManageIndexPatterns = + newPlatform.application.capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = newPlatform.overlays.banners.replace(bannerId, (element: HTMLElement) => { + ReactDOM.render( + + + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); + }); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + newPlatform.overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + kbnUrl.change(redirectTarget); + $rootScope.$digest(); + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); + } +} diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts index b29056954051b..ea8932114118e 100644 --- a/src/legacy/ui/public/legacy_compat/index.ts +++ b/src/legacy/ui/public/legacy_compat/index.ts @@ -18,3 +18,4 @@ */ export { configureAppAngularModule } from './angular_config'; +export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx index f0ac787e0ef44..cd3d85090dce0 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.tsx +++ b/src/legacy/ui/public/management/components/sidebar_nav.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiIcon, EuiSideNav, IconType } from '@elastic/eui'; +import { EuiIcon, EuiSideNav, IconType, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -72,17 +72,26 @@ export class SidebarNav extends React.Component + <> + +

+ {i18n.translate('common.ui.management.nav.label', { + defaultMessage: 'Management', + })} +

+ + + ); } diff --git a/src/legacy/ui/public/management/index.d.ts b/src/legacy/ui/public/management/index.d.ts index b5d927cd90433..7880e1d5d0295 100644 --- a/src/legacy/ui/public/management/index.d.ts +++ b/src/legacy/ui/public/management/index.d.ts @@ -21,10 +21,10 @@ declare module 'ui/management' { export const PAGE_TITLE_COMPONENT: string; export const PAGE_SUBTITLE_COMPONENT: string; export const PAGE_FOOTER_COMPONENT: string; - export const SidebarNav: React.SFC; + export const SidebarNav: React.FC; export function registerSettingsComponent( id: string, - component: string | React.SFC, + component: string | React.FC, allowOverride: boolean ): void; export const management: any; // TODO - properly provide types diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 5c269c7b019aa..ff89ef69d53ca 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -18,16 +18,36 @@ */ import sinon from 'sinon'; +import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; const mockObservable = () => { return { - subscribe: () => {} + subscribe: () => {}, }; }; +const mockComponent = () => { + return null; +}; + +export const mockUiSettings = { + get: (item) => { + return mockUiSettings[item]; + }, + getUpdate$: () => ({ + subscribe: sinon.fake(), + }), + 'query:allowLeadingWildcards': true, + 'query:queryString:options': {}, + 'courier:ignoreFilterIfFieldNotInIndex': true, + 'dateFormat:tz': 'Browser', + 'format:defaultTypeMap': {}, +}; + export const npSetup = { core: { - chrome: {} + chrome: {}, + uiSettings: mockUiSettings, }, plugins: { embeddable: { @@ -59,15 +79,20 @@ export const npSetup = { timefilter: { timefilter: sinon.fake(), history: sinon.fake(), - } + }, }, + fieldFormats: getFieldFormatsRegistry(mockUiSettings), }, share: { register: () => {}, }, - devTools: { + dev_tools: { register: () => {}, }, + kibana_legacy: { + registerLegacyApp: () => {}, + forwardApp: () => {}, + }, inspector: { registerView: () => undefined, __LEGACY: { @@ -81,6 +106,9 @@ export const npSetup = { registerAction: sinon.fake(), registerTrigger: sinon.fake(), }, + feature_catalogue: { + register: sinon.fake(), + }, }, }; @@ -90,7 +118,7 @@ let isAutoRefreshSelectorEnabled = true; export const npStart = { core: { - chrome: {} + chrome: {}, }, plugins: { embeddable: { @@ -103,14 +131,21 @@ export const npStart = { registerRenderer: sinon.fake(), registerType: sinon.fake(), }, - devTools: { + dev_tools: { getSortedDevTools: () => [], }, + kibana_legacy: { + getApps: () => [], + getForwards: () => [], + }, data: { autocomplete: { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + ui: { + IndexPatternSelect: mockComponent, + }, query: { filterManager: { getFetches$: sinon.fake(), @@ -122,7 +157,6 @@ export const npStart = { setFilters: sinon.fake(), removeAll: sinon.fake(), getUpdates$: mockObservable, - }, timefilter: { timefilter: { @@ -146,7 +180,7 @@ export const npStart = { getRefreshInterval: () => { return refreshInterval; }, - setRefreshInterval: (interval) => { + setRefreshInterval: interval => { refreshInterval = interval; }, enableTimeRangeSelector: () => { @@ -164,6 +198,7 @@ export const npStart = { history: sinon.fake(), }, }, + fieldFormats: getFieldFormatsRegistry(mockUiSettings), }, share: { toggleShareContextMenu: () => {}, @@ -185,6 +220,9 @@ export const npStart = { getTriggerActions: sinon.fake(), getTriggerCompatibleActions: sinon.fake(), }, + feature_catalogue: { + register: sinon.fake(), + }, }, }; diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index cbdaccd65f94b..e5d5cd0a87776 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -18,13 +18,15 @@ */ import { setRootControllerMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__ } from './new_platform'; +import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { coreMock } from '../../../../core/public/mocks'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { beforeEach(() => { setRootControllerMock.mockReset(); __reset__(); + __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); }); const registerApp = () => { @@ -59,7 +61,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], - appBasePath: '', + appBasePath: '/test/base/path/app/test', }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 6e71d36877895..c0b2d6d913257 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -19,7 +19,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; -import { Start as EmbeddableStart, Setup as EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -29,33 +29,33 @@ import { } from '../../../../plugins/inspector/public'; import { EuiUtilsStart } from '../../../../plugins/eui_utils/public'; import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/public'; -import { - FeatureCatalogueSetup, - FeatureCatalogueStart, -} from '../../../../plugins/feature_catalogue/public'; +import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; +import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public'; import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; export interface PluginsSetup { data: ReturnType; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ReturnType; - feature_catalogue: FeatureCatalogueSetup; + home: HomePublicPluginSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; + dev_tools: DevToolsSetup; + kibana_legacy: KibanaLegacySetup; share: SharePluginSetup; - devTools: DevToolsSetup; } export interface PluginsStart { data: ReturnType; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; eui_utils: EuiUtilsStart; expressions: ReturnType; - feature_catalogue: FeatureCatalogueStart; + home: HomePublicPluginStart; inspector: InspectorStart; uiActions: IUiActionsStart; + dev_tools: DevToolsStart; + kibana_legacy: KibanaLegacyStart; share: SharePluginStart; - devTools: DevToolsStart; } export const npSetup = { @@ -111,7 +111,10 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const unmount = await app.mount( + { core: npStart.core }, + { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } + ); $scope.$on('$destroy', () => { unmount(); }); diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts index 8ef97c0b3ebcc..3d435f2ba8dfd 100644 --- a/src/legacy/ui/public/promises/defer.ts +++ b/src/legacy/ui/public/promises/defer.ts @@ -17,7 +17,7 @@ * under the License. */ -interface Defer { +export interface Defer { promise: Promise; resolve(value: T): void; reject(reason: Error): void; diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js index 8905a15106953..475705ff39e48 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ b/src/legacy/ui/public/registry/feature_catalogue.js @@ -19,7 +19,7 @@ import { uiRegistry } from './_registry'; import { capabilities } from '../capabilities'; -export { FeatureCatalogueCategory } from '../../../../plugins/feature_catalogue/public'; +export { FeatureCatalogueCategory } from '../../../../plugins/home/public'; export const FeatureCatalogueRegistryProvider = uiRegistry({ name: 'featureCatalogue', diff --git a/src/legacy/ui/public/registry/field_formats.d.ts b/src/legacy/ui/public/registry/field_formats.d.ts deleted file mode 100644 index 79eec7a5a4e74..0000000000000 --- a/src/legacy/ui/public/registry/field_formats.d.ts +++ /dev/null @@ -1,20 +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. - */ - -export type FieldFormat = any; diff --git a/src/legacy/ui/public/registry/field_formats.js b/src/legacy/ui/public/registry/field_formats.js deleted file mode 100644 index 9e03ef2e3cdd9..0000000000000 --- a/src/legacy/ui/public/registry/field_formats.js +++ /dev/null @@ -1,186 +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 { memoize, forOwn, isFunction } from 'lodash'; -import { npStart } from 'ui/new_platform'; - -class FieldFormatRegistry { - constructor() { - this.fieldFormats = new Map(); - this._uiSettings = npStart.core.uiSettings; - this.getConfig = (...args) => this._uiSettings.get(...args); - this._defaultMap = []; - this.init(); - } - - init() { - this.parseDefaultTypeMap(this._uiSettings.get('format:defaultTypeMap')); - - this._uiSettings.getUpdate$().subscribe(({ key, newValue }) => { - if (key === 'format:defaultTypeMap') { - this.parseDefaultTypeMap(newValue); - } - }); - } - - /** - * Get the id of the default type for this field type - * using the format:defaultTypeMap config map - * - * @param {String} fieldType - the field type - * @param {String[]} esTypes - Array of ES data types - * @return {Object} - */ - getDefaultConfig = (fieldType, esTypes) => { - const type = this.getDefaultTypeName(fieldType, esTypes); - return this._defaultMap[type] || this._defaultMap._default_; - }; - - /** - * Get a FieldFormat type (class) by it's id. - * - * @param {String} formatId - the format id - * @return {Function} - */ - getType = (formatId) => { - return this.fieldFormats.get(formatId); - }; - /** - * Get the default FieldFormat type (class) for - * a field type, using the format:defaultTypeMap. - * used by the field editor - * - * @param {String} fieldType - * @param {String} esTypes - Array of ES data types - * @return {Function} - */ - getDefaultType = (fieldType, esTypes) => { - const config = this.getDefaultConfig(fieldType, esTypes); - return this.getType(config.id); - }; - - /** - * Get the name of the default type for ES types like date_nanos - * using the format:defaultTypeMap config map - * - * @param {String[]} esTypes - Array of ES data types - * @return {String|undefined} - */ - getTypeNameByEsTypes = (esTypes) => { - if(!Array.isArray(esTypes)) { - return; - } - return esTypes.find(type => this._defaultMap[type] && this._defaultMap[type].es); - }; - /** - * Get the default FieldFormat type name for - * a field type, using the format:defaultTypeMap. - * - * @param {String} fieldType - * @param {String[]} esTypes - * @return {string} - */ - getDefaultTypeName = (fieldType, esTypes) => { - return this.getTypeNameByEsTypes(esTypes) || fieldType; - }; - - /** - * Get the singleton instance of the FieldFormat type by it's id. - * - * @param {String} formatId - * @return {FieldFormat} - */ - getInstance = memoize(function (formatId) { - const FieldFormat = this.getType(formatId); - if (!FieldFormat) { - throw new Error(`Field Format '${formatId}' not found!`); - } - return new FieldFormat(null, this.getConfig); - }); - - /** - * Get the default fieldFormat instance for a field format. - * - * @param {String} fieldType - * @param {String[]} esTypes - * @return {FieldFormat} - */ - getDefaultInstancePlain(fieldType, esTypes) { - const conf = this.getDefaultConfig(fieldType, esTypes); - - const FieldFormat = this.getType(conf.id); - return new FieldFormat(conf.params, this.getConfig); - } - /** - * Returns a cache key built by the given variables for caching in memoized - * Where esType contains fieldType, fieldType is returned - * -> kibana types have a higher priority in that case - * -> would lead to failing tests that match e.g. date format with/without esTypes - * https://lodash.com/docs#memoize - * - * @param {String} fieldType - * @param {String[]} esTypes - * @return {string} - */ - getDefaultInstanceCacheResolver(fieldType, esTypes) { - return Array.isArray(esTypes) && esTypes.indexOf(fieldType) === -1 - ? [fieldType, ...esTypes].join('-') - : fieldType; - } - - /** - * Get filtered list of field formats by format type - * - * @param {String} fieldType - * @return {FieldFormat[]} - */ - - getByFieldType(fieldType) { - return [ ...this.fieldFormats.values()] - .filter(format => format.fieldType.indexOf(fieldType) !== -1); - } - - /** - * Get the default fieldFormat instance for a field format. - * It's a memoized function that builds and reads a cache - * - * @param {String} fieldType - * @param {String[]} esTypes - * @return {FieldFormat} - */ - getDefaultInstance = memoize(this.getDefaultInstancePlain, this.getDefaultInstanceCacheResolver); - - parseDefaultTypeMap(value) { - this._defaultMap = value; - forOwn(this, function (fn) { - if (isFunction(fn) && fn.cache) { - // clear all memoize caches - fn.cache = new memoize.Cache(); - } - }); - } - - register = derivedFieldFormat => { - this.fieldFormats.set(derivedFieldFormat.id, derivedFieldFormat); - - return this; - }; -} - -export const fieldFormats = new FieldFormatRegistry(); diff --git a/src/legacy/ui/public/routes/__tests__/_route_manager.js b/src/legacy/ui/public/routes/__tests__/_route_manager.js index d6d4c869b4b7e..450bb51f0b0c6 100644 --- a/src/legacy/ui/public/routes/__tests__/_route_manager.js +++ b/src/legacy/ui/public/routes/__tests__/_route_manager.js @@ -119,18 +119,6 @@ describe('routes/route_manager', function () { expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false); expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true); }); - - it('sets route.requireDefaultIndex to false by default', function () { - routes.when('/nothing-set'); - routes.when('/no-index-required', { requireDefaultIndex: false }); - routes.when('/index-required', { requireDefaultIndex: true }); - routes.config($rp); - - expect($rp.when.callCount).to.be(3); - expect($rp.when.firstCall.args[1]).to.have.property('requireDefaultIndex', false); - expect($rp.when.secondCall.args[1]).to.have.property('requireDefaultIndex', false); - expect($rp.when.lastCall.args[1]).to.have.property('requireDefaultIndex', true); - }); }); describe('#defaults()', () => { diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts index 56203354f3c20..a5261a7c8ee3a 100644 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ b/src/legacy/ui/public/routes/route_manager.d.ts @@ -23,7 +23,7 @@ import { ChromeBreadcrumb } from '../../../../core/public'; -interface RouteConfiguration { +export interface RouteConfiguration { controller?: string | ((...args: any[]) => void); redirectTo?: string; resolveRedirectTo?: (...args: any[]) => void; diff --git a/src/legacy/ui/public/routes/route_manager.js b/src/legacy/ui/public/routes/route_manager.js index ba48984bb45b9..6444ef66fbe47 100644 --- a/src/legacy/ui/public/routes/route_manager.js +++ b/src/legacy/ui/public/routes/route_manager.js @@ -46,10 +46,6 @@ export default function RouteManager() { route.reloadOnSearch = false; } - if (route.requireDefaultIndex == null) { - route.requireDefaultIndex = false; - } - wrapRouteWithPrep(route, setup); $routeProvider.when(path, route); }); diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index ff53d48de758a..56124a047ba6d 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -26,6 +26,7 @@ import { SavedObjectProvider } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; +import { mockUiSettings } from '../../new_platform/new_platform.karma_mock'; const getConfig = cfg => cfg; @@ -337,7 +338,7 @@ describe('Saved Object', function () { type: 'dashboard', }); }); - const indexPattern = new StubIndexPattern('my-index', getConfig, null, []); + const indexPattern = new StubIndexPattern('my-index', getConfig, null, [], mockUiSettings); indexPattern.title = indexPattern.id; savedObject.searchSource.setField('index', indexPattern); return savedObject @@ -725,7 +726,7 @@ describe('Saved Object', function () { const savedObject = new SavedObject(config); sinon.stub(savedObject, 'hydrateIndexPattern').callsFake(() => { - const indexPattern = new StubIndexPattern(indexPatternId, getConfig, null, []); + const indexPattern = new StubIndexPattern(indexPatternId, getConfig, null, [], mockUiSettings); indexPattern.title = indexPattern.id; savedObject.searchSource.setField('index', indexPattern); return Promise.resolve(indexPattern); diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index b7623ab0fc5a5..8d55a6929a617 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -42,9 +42,14 @@ import { isStateHash, } from './state_storage'; -export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl) { +export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, $injector) { const Events = Private(EventsProvider); + const isDummyRoute = () => + $injector.has('$route') && + $injector.get('$route').current && + $injector.get('$route').current.outerAngularWrapperRoute; + createLegacyClass(State).inherits(Events); function State( urlParam, @@ -137,7 +142,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon let stash = this._readFromURL(); - // nothing to read from the url? save if ordered to persist + // nothing to read from the url? save if ordered to persist, but only if it's not on a wrapper route if (stash === null) { if (this._persistAcrossApps) { return this.save(); @@ -150,7 +155,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon // apply diff to state from stash, will change state in place via side effect const diffResults = applyDiff(this, stash); - if (diffResults.keys.length) { + if (!isDummyRoute() && diffResults.keys.length) { this.emit('fetch_with_changes', diffResults.keys); } }; @@ -164,6 +169,10 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon return; } + if (isDummyRoute()) { + return; + } + let stash = this._readFromURL(); const state = this.toObject(); replace = replace || false; diff --git a/src/legacy/ui/public/time_buckets/time_buckets.js b/src/legacy/ui/public/time_buckets/time_buckets.js index bcd5cd161edd3..c875d7820373e 100644 --- a/src/legacy/ui/public/time_buckets/time_buckets.js +++ b/src/legacy/ui/public/time_buckets/time_buckets.js @@ -20,13 +20,14 @@ import _ from 'lodash'; import moment from 'moment'; import chrome from '../chrome'; +import { npStart } from 'ui/new_platform'; import { parseInterval } from '../utils/parse_interval'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { convertDurationToNormalizedEsInterval, convertIntervalToEsInterval, } from './calc_es_interval'; -import { fieldFormats } from '../registry/field_formats'; +import { FIELD_FORMAT_IDS } from '../../../../plugins/data/public'; const config = chrome.getUiSettingsClient(); @@ -311,7 +312,9 @@ TimeBuckets.prototype.getScaledDateFormat = function () { }; TimeBuckets.prototype.getScaledDateFormatter = function () { - const DateFieldFormat = fieldFormats.getType('date'); + const fieldFormats = npStart.plugins.data.fieldFormats; + const DateFieldFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE); + return new DateFieldFormat({ pattern: this.getScaledDateFormat() }, getConfig); diff --git a/src/legacy/ui/public/timefilter/setup_router.test.js b/src/legacy/ui/public/timefilter/setup_router.test.js index 4bc797e5eff00..f229937c3b435 100644 --- a/src/legacy/ui/public/timefilter/setup_router.test.js +++ b/src/legacy/ui/public/timefilter/setup_router.test.js @@ -42,9 +42,14 @@ describe('registerTimefilterWithGlobalState()', () => { } }; + const rootScope = { + $on: jest.fn() + }; + registerTimefilterWithGlobalState( timefilter, - globalState + globalState, + rootScope, ); expect(setTime.mock.calls.length).toBe(2); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts index 0a73378f99cd7..64105b016fb44 100644 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ b/src/legacy/ui/public/timefilter/setup_router.ts @@ -23,6 +23,7 @@ import moment from 'moment'; import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; import chrome from 'ui/chrome'; import { RefreshInterval, TimeRange, TimefilterContract } from 'src/plugins/data/public'; +import { Subscription } from 'rxjs'; // TODO // remove everything underneath once globalState is no longer an angular service @@ -40,49 +41,62 @@ export function getTimefilterConfig() { }; } -// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter -// and require it to be executed to properly function. -// This function is exposed for applications that do not use uiRoutes like APM -// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter -export const registerTimefilterWithGlobalState = _.once( - (timefilter: TimefilterContract, globalState: any, $rootScope: IScope) => { - // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. - const config = getTimefilterConfig(); - timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); - timefilter.setRefreshInterval( - _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) - ); +export const registerTimefilterWithGlobalStateFactory = ( + timefilter: TimefilterContract, + globalState: any, + $rootScope: IScope +) => { + // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. + const config = getTimefilterConfig(); + timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); + timefilter.setRefreshInterval( + _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) + ); - globalState.on('fetch_with_changes', () => { - // clone and default to {} in one - const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); - const newRefreshInterval: RefreshInterval = _.defaults( - {}, - globalState.refreshInterval, - config.refreshIntervalDefaults - ); + globalState.on('fetch_with_changes', () => { + // clone and default to {} in one + const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); + const newRefreshInterval: RefreshInterval = _.defaults( + {}, + globalState.refreshInterval, + config.refreshIntervalDefaults + ); - if (newTime) { - if (newTime.to) newTime.to = convertISO8601(newTime.to); - if (newTime.from) newTime.from = convertISO8601(newTime.from); - } + if (newTime) { + if (newTime.to) newTime.to = convertISO8601(newTime.to); + if (newTime.from) newTime.from = convertISO8601(newTime.from); + } - timefilter.setTime(newTime); - timefilter.setRefreshInterval(newRefreshInterval); - }); + timefilter.setTime(newTime); + timefilter.setRefreshInterval(newRefreshInterval); + }); - const updateGlobalStateWithTime = () => { - globalState.time = timefilter.getTime(); - globalState.refreshInterval = timefilter.getRefreshInterval(); - globalState.save(); - }; + const updateGlobalStateWithTime = () => { + globalState.time = timefilter.getTime(); + globalState.refreshInterval = timefilter.getRefreshInterval(); + globalState.save(); + }; + const subscriptions = new Subscription(); + subscriptions.add( subscribeWithScope($rootScope, timefilter.getRefreshIntervalUpdate$(), { next: updateGlobalStateWithTime, - }); + }) + ); + subscriptions.add( subscribeWithScope($rootScope, timefilter.getTimeUpdate$(), { next: updateGlobalStateWithTime, - }); - } -); + }) + ); + + $rootScope.$on('$destroy', () => { + subscriptions.unsubscribe(); + }); +}; + +// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter +// and require it to be executed to properly function. +// This function is exposed for applications that do not use uiRoutes like APM +// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter +export const registerTimefilterWithGlobalState = _.once(registerTimefilterWithGlobalStateFactory); diff --git a/src/legacy/ui/public/vis/__tests__/_agg_config.js b/src/legacy/ui/public/vis/__tests__/_agg_config.js index 2e2e0c31bdb8a..9b0398cf8853e 100644 --- a/src/legacy/ui/public/vis/__tests__/_agg_config.js +++ b/src/legacy/ui/public/vis/__tests__/_agg_config.js @@ -24,7 +24,6 @@ import { Vis } from '..'; import { AggType } from '../../agg_types/agg_type'; import { AggConfig } from '../../agg_types/agg_config'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { fieldFormats } from '../../registry/field_formats'; describe('AggConfig', function () { @@ -439,12 +438,15 @@ describe('AggConfig', function () { } ] }); - expect(vis.aggs.aggs[0].fieldFormatter()).to.be(fieldFormats.getDefaultInstance('number').getConverterFor()); + + const fieldFormatter = vis.aggs.aggs[0].fieldFormatter(); + + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); }); }); describe('#fieldFormatter - no custom getFormat handler', function () { - const visStateAggWithoutCustomGetFormat = { aggs: [ { @@ -467,13 +469,17 @@ describe('AggConfig', function () { it('returns the string format if the field does not have a format', function () { const agg = vis.aggs.aggs[0]; agg.params.field = { type: 'number', format: null }; - expect(agg.fieldFormatter()).to.be(fieldFormats.getDefaultInstance('string').getConverterFor()); + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); }); it('returns the string format if their is no field', function () { const agg = vis.aggs.aggs[0]; delete agg.params.field; - expect(agg.fieldFormatter()).to.be(fieldFormats.getDefaultInstance('string').getConverterFor()); + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); }); it('returns the html converter if "html" is passed in', function () { diff --git a/src/legacy/ui/public/vis/vis_filters/vis_filters.js b/src/legacy/ui/public/vis/vis_filters/vis_filters.js index e879d040125f1..18d633e1b5fb2 100644 --- a/src/legacy/ui/public/vis/vis_filters/vis_filters.js +++ b/src/legacy/ui/public/vis/vis_filters/vis_filters.js @@ -115,7 +115,6 @@ const VisFiltersProvider = (getAppState, $timeout) => { } }; - return { pushFilters, }; diff --git a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx index ebbe886b3650b..19cbbf9cea04c 100644 --- a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx +++ b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx @@ -19,7 +19,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { SearchError } from 'ui/courier'; +import { SearchError } from '../../courier'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; interface VisualizationRequestErrorProps { @@ -32,7 +32,7 @@ export class VisualizationRequestError extends React.Component diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index 70e0c1f1382fa..608a8b9ce8aa7 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -28,7 +28,7 @@ import { } from './build_pipeline'; import { Vis, VisState } from 'ui/vis'; import { AggConfig } from 'ui/agg_types/agg_config'; -import { searchSourceMock } from 'ui/courier/search_source/mocks'; +import { searchSourceMock } from '../../../courier/search_source/mocks'; jest.mock('ui/new_platform'); jest.mock('ui/agg_types/buckets/date_histogram', () => ({ diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 21b13abea440e..ca9540b4d3737 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -20,11 +20,11 @@ import { cloneDeep, get } from 'lodash'; // @ts-ignore import { setBounds } from 'ui/agg_types'; -import { SearchSource } from 'ui/courier'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; import { isDateHistogramBucketAggConfig } from 'ui/agg_types/buckets/date_histogram'; import moment from 'moment'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../courier/types'; import { createFormat } from './utilities'; interface SchemaConfigParams { @@ -462,7 +462,7 @@ export const buildVislibDimensions = async ( // take a Vis object and decorate it with the necessary params (dimensions, bucket, metric, etc) export const getVisParams = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any; abortSignal?: AbortSignal } + params: { searchSource: SearchSourceContract; timeRange?: any; abortSignal?: AbortSignal } ) => { const schemas = getSchemas(vis, params.timeRange); let visConfig = cloneDeep(vis.params); @@ -479,7 +479,10 @@ export const getVisParams = async ( export const buildPipeline = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any } + params: { + searchSource: SearchSourceContract; + timeRange?: any; + } ) => { const { searchSource } = params; const { indexPattern } = vis; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index 6598da76f60ba..f49e0f08e8732 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -20,14 +20,13 @@ import { i18n } from '@kbn/i18n'; import { identity } from 'lodash'; import { AggConfig, Vis } from 'ui/vis'; +import { npStart } from 'ui/new_platform'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; -import { FieldFormat } from '../../../../../../plugins/data/public'; +import { IFieldFormatId, FieldFormat } from '../../../../../../plugins/data/public'; import { tabifyGetColumns } from '../../../agg_response/tabify/_get_columns'; import chrome from '../../../chrome'; -// @ts-ignore -import { fieldFormats } from '../../../registry/field_formats'; import { dateRange } from '../../../utils/date_range'; import { ipRange } from '../../../utils/ip_range'; import { DateRangeKey } from '../../../agg_types/buckets/date_range'; @@ -45,18 +44,22 @@ function isTermsFieldFormat( return serializedFieldFormat.id === 'terms'; } -const config = chrome.getUiSettingsClient(); +const getConfig = (key: string, defaultOverride?: any): any => + npStart.core.uiSettings.get(key, defaultOverride); +const DefaultFieldFormat = FieldFormat.from(identity); -const getConfig = (...args: any[]): any => config.get(...args); -const getDefaultFieldFormat = () => ({ convert: identity }); +const getFieldFormat = (id?: IFieldFormatId, params: object = {}): FieldFormat => { + const fieldFormats = npStart.plugins.data.fieldFormats; -const getFieldFormat = (id: string | undefined, params: object = {}) => { - const Format = fieldFormats.getType(id); - if (Format) { - return new Format(params, getConfig); - } else { - return getDefaultFieldFormat(); + if (id) { + const Format = fieldFormats.getType(id); + + if (Format) { + return new Format(params, getConfig); + } } + + return new DefaultFieldFormat(); }; export const createFormat = (agg: AggConfig): SerializedFieldFormat => { @@ -93,9 +96,9 @@ export const createFormat = (agg: AggConfig): SerializedFieldFormat => { export type FormatFactory = (mapping?: SerializedFieldFormat) => FieldFormat; -export const getFormat: FormatFactory = (mapping = {}) => { +export const getFormat: FormatFactory = mapping => { if (!mapping) { - return getDefaultFieldFormat(); + return new DefaultFieldFormat(); } const { id } = mapping; if (id === 'range') { @@ -145,6 +148,7 @@ export const getFormat: FormatFactory = (mapping = {}) => { pathname: window.location.pathname, basePath: chrome.getBasePath(), }; + // @ts-ignore return format.convert(val, undefined, undefined, parsedUrl); }; }, @@ -161,9 +165,10 @@ export const getFormat: FormatFactory = (mapping = {}) => { pathname: window.location.pathname, basePath: chrome.getBasePath(), }; + // @ts-ignore return format.convert(val, type, undefined, parsedUrl); }, - }; + } as FieldFormat; } else { return getFieldFormat(id, mapping.params); } diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 36759551a1723..a9203415321fa 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -24,13 +24,13 @@ import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../../../vis'; +import { SearchSource, SearchSourceContract } from '../../../courier'; import { esFilters, Query } from '../../../../../../plugins/data/public'; -import { SearchSource } from '../../../courier'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; query?: Query; - searchSource?: SearchSource; + searchSource?: SearchSourceContract; } /** diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0d05ea259d1a1..763167c6b5ccf 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -17,6 +17,7 @@ * under the License. */ +import { take } from 'rxjs/operators'; import { createHash } from 'crypto'; import { props, reduce as reduceAsync } from 'bluebird'; import Boom from 'boom'; @@ -42,21 +43,31 @@ export function uiRenderMixin(kbnServer, server, config) { let defaultInjectedVars = {}; kbnServer.afterPluginsInit(() => { const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; - defaultInjectedVars = defaultInjectedVarProviders - .reduce((allDefaults, { fn, pluginSpec }) => ( + defaultInjectedVars = defaultInjectedVarProviders.reduce( + (allDefaults, { fn, pluginSpec }) => mergeVariables( allDefaults, fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) - ) - ), {}); + ), + {} + ); }); // render all views from ./views server.setupViews(resolve(__dirname, 'views')); - server.exposeStaticDir('/node_modules/@elastic/eui/dist/{path*}', fromRoot('node_modules/@elastic/eui/dist')); - server.exposeStaticDir('/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist')); - server.exposeStaticDir('/node_modules/@elastic/charts/dist/{path*}', fromRoot('node_modules/@elastic/charts/dist')); + server.exposeStaticDir( + '/node_modules/@elastic/eui/dist/{path*}', + fromRoot('node_modules/@elastic/eui/dist') + ); + server.exposeStaticDir( + '/node_modules/@kbn/ui-framework/dist/{path*}', + fromRoot('node_modules/@kbn/ui-framework/dist') + ); + server.exposeStaticDir( + '/node_modules/@elastic/charts/dist/{path*}', + fromRoot('node_modules/@elastic/charts/dist') + ); const translationsCache = { translations: null, hash: null }; server.route({ @@ -80,11 +91,12 @@ export function uiRenderMixin(kbnServer, server, config) { .digest('hex'); } - return h.response(translationsCache.translations) + return h + .response(translationsCache.translations) .header('cache-control', 'must-revalidate') .header('content-type', 'application/json') .etag(translationsCache.hash); - } + }, }); // register the bootstrap.js route after plugins are initialized so that we can @@ -105,42 +117,38 @@ export function uiRenderMixin(kbnServer, server, config) { const isCore = !app; const uiSettings = request.getUiSettingsService(); - const darkMode = !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:darkMode') - : false; + const darkMode = + !authEnabled || request.auth.isAuthenticated + ? await uiSettings.get('theme:darkMode') + : false; const basePath = config.get('server.basePath'); const regularBundlePath = `${basePath}/bundles`; const dllBundlePath = `${basePath}/built_assets/dlls`; const styleSheetPaths = [ `${dllBundlePath}/vendors.style.dll.css`, - ...( - darkMode ? - [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, - ] : [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, - ] - ), + ...(darkMode + ? [ + `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, + ] + : [ + `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, + ]), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - ...( - !isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : [] - ), + ...(!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []), ...kbnServer.uiExports.styleSheetPaths - .filter(path => ( - path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') - )) - .map(path => ( + .filter(path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')) + .map(path => path.localPath.endsWith('.scss') ? `${basePath}/built_assets/css/${path.publicPath}` : `${basePath}/${path.publicPath}` - )) - .reverse() + ) + .reverse(), ]; const bootstrap = new AppBootstrap({ @@ -149,17 +157,18 @@ export function uiRenderMixin(kbnServer, server, config) { regularBundlePath, dllBundlePath, styleSheetPaths, - } + }, }); const body = await bootstrap.getJsFile(); const etag = await bootstrap.getJsFileHash(); - return h.response(body) + return h + .response(body) .header('cache-control', 'must-revalidate') .header('content-type', 'application/javascript') .etag(etag); - } + }, }); }); @@ -179,14 +188,14 @@ export function uiRenderMixin(kbnServer, server, config) { } catch (err) { throw Boom.boomify(err); } - } + }, }); async function getUiSettings({ request, includeUserProvidedConfig }) { const uiSettings = request.getUiSettingsService(); return props({ defaults: uiSettings.getRegistered(), - user: includeUserProvidedConfig && uiSettings.getUserProvided() + user: includeUserProvidedConfig && uiSettings.getUserProvided(), }); } @@ -206,7 +215,12 @@ export function uiRenderMixin(kbnServer, server, config) { }; } - async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { + async function renderApp({ + app, + h, + includeUserProvidedConfig = true, + injectedVarsOverrides = {}, + }) { const request = h.request; const basePath = request.getBasePath(); const uiSettings = await getUiSettings({ request, includeUserProvidedConfig }); @@ -215,14 +229,22 @@ export function uiRenderMixin(kbnServer, server, config) { const legacyMetadata = getLegacyKibanaPayload({ app, basePath, - uiSettings + uiSettings, }); // Get the list of new platform plugins. // Convert the Map into an array of objects so it is JSON serializable and order is preserved. - const uiPlugins = [ - ...kbnServer.newPlatform.__internals.uiPlugins.public.entries() - ].map(([id, plugin]) => ({ id, plugin })); + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.browserConfigs; + const uiPlugins = await Promise.all([ + ...kbnServer.newPlatform.__internals.uiPlugins.public.entries(), + ].map(async ([id, plugin]) => { + const config$ = uiPluginConfigs.get(id); + if (config$) { + return { id, plugin, config: await config$.pipe(take(1)).toPromise() }; + } else { + return { id, plugin, config: {} }; + } + })); const response = h.view('ui_app', { strictCsp: config.get('csp.strict'), @@ -250,8 +272,8 @@ export function uiRenderMixin(kbnServer, server, config) { mergeVariables( injectedVarsOverrides, app ? await server.getInjectedUiAppVars(app.getId()) : {}, - defaultInjectedVars, - ), + defaultInjectedVars + ) ), uiPlugins, diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 28eb448d12d82..2eaf4c1d6e882 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -61,7 +61,7 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.discoveredPlugins = opts.discoveredPlugins; + this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -551,9 +551,9 @@ export default class BaseOptimizer { _getDiscoveredPluginEntryPoints() { // New platform plugin entry points - return [...this.discoveredPlugins.entries()] - .reduce((entryPoints, [pluginId, plugin]) => { - entryPoints[`plugin/${pluginId}`] = `${plugin.path}/public`; + return [...this.newPlatformPluginInfo.entries()] + .reduce((entryPoints, [pluginId, pluginInfo]) => { + entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; return entryPoints; }, {}); } diff --git a/src/optimize/index.js b/src/optimize/index.js index 9789e7abc2f9d..0960f9ecb10b6 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -66,7 +66,7 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - discoveredPlugins: newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index 16be840b3ca0e..9fbeceb578615 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -30,7 +30,7 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - discoveredPlugins: kbnServer.newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/plugins/apm_oss/kibana.json b/src/plugins/apm_oss/kibana.json new file mode 100644 index 0000000000000..5853ba198e717 --- /dev/null +++ b/src/plugins/apm_oss/kibana.json @@ -0,0 +1,11 @@ +{ + "id": "apm_oss", + "version": "8.0.0", + "server": true, + "kibanaVersion": "kibana", + "configPath": [ + "apm_oss" + ], + "ui": false, + "requiredPlugins": [] +} diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts new file mode 100644 index 0000000000000..801140694c139 --- /dev/null +++ b/src/plugins/apm_oss/server/index.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '../../../core/server'; +import { APMOSSPlugin } from './plugin'; + +export const config = { + schema: schema.object({ + transactionIndices: schema.string({ defaultValue: 'apm-*' }), + spanIndices: schema.string({ defaultValue: 'apm-*' }), + errorIndices: schema.string({ defaultValue: 'apm-*' }), + metricsIndices: schema.string({ defaultValue: 'apm-*' }), + sourcemapIndices: schema.string({ defaultValue: 'apm-*' }), + onboardingIndices: schema.string({ defaultValue: 'apm-*' }), + indexPattern: schema.string({ defaultValue: 'apm-*' }), + }), +}; + +export function plugin(initializerContext: PluginInitializerContext) { + return new APMOSSPlugin(initializerContext); +} + +export type APMOSSConfig = TypeOf; + +export { APMOSSPlugin as Plugin }; diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts new file mode 100644 index 0000000000000..2708f7729482b --- /dev/null +++ b/src/plugins/apm_oss/server/plugin.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Observable } from 'rxjs'; +import { APMOSSConfig } from './'; + +export class APMOSSPlugin implements Plugin<{ config$: Observable }> { + constructor(private readonly initContext: PluginInitializerContext) { + this.initContext = initContext; + } + + public setup(core: CoreSetup) { + const config$ = this.initContext.config.create(); + + return { + config$, + }; + } + + start() {} + stop() {} +} diff --git a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx index b30733760bbdf..f15d538703e21 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - Start as EmbeddableStart, + IEmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index f6d2fcbcd57fd..78ce6bdc4c58f 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, Start as EmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements IAction { private core: CoreStart, private savedobjectfinder: React.ComponentType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ActionContext) { diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx index 36efd0bcba676..36313353e3c33 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx @@ -20,15 +20,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; -import { - IContainer, - IEmbeddable, - EmbeddableInput, - EmbeddableOutput, - Start as EmbeddableStart, -} from '../embeddable_plugin'; +import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; interface Props { container: IContainer; @@ -36,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: GetEmbeddableFactories; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 6cefd11c912f1..684aa93779bc1 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - Start as EmbeddableStartContract, + IEmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: EmbeddableStartContract; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss index 7cbe135115877..9575908146d1d 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss @@ -1,8 +1,10 @@ .dshDashboardViewport { + height: 100%; width: 100%; background-color: $euiColorEmptyShade; } .dshDashboardViewport-withMargins { width: 100%; + height: 100%; } diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index dbb5a06da9cd9..79cc9b6980545 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { IUiActionsSetup, IUiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -34,12 +34,12 @@ import { } from '../../../plugins/kibana_react/public'; interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; uiActions: IUiActionsStart; } diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index 3db23051b6ced..405754ffcb572 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -18,7 +18,7 @@ */ import { buildEsQuery } from './build_es_query'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; import { IIndexPattern } from '../../index_patterns'; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index b754496793660..e4f5f1f9e216c 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -41,7 +41,7 @@ export interface EsQueryConfig { * config contains dateformat:tz */ export function buildEsQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query | Query[], filters: Filter | Filter[], config: EsQueryConfig = { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6a5c7bdf8eea3..669c5a62af726 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,9 +31,8 @@ describe('filterMatchesIndex', () => { it('should return true if no index pattern is passed', () => { const filter = { meta: { index: 'foo', key: 'bar' } } as Filter; - const indexPattern = null; - expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + expect(filterMatchesIndex(filter, undefined)).toBe(true); }); it('should return true if the filter key matches a field name', () => { @@ -49,4 +48,11 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); + + it('should return true if the filter has meta without a key', () => { + const filter = { meta: { index: 'foo' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); }); diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 496aab3ea585f..a9cd3d8b7ba26 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -25,8 +25,8 @@ import { Filter } from '../filters'; * this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking * change. */ -export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { - if (!filter.meta || !indexPattern) { +export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) { + if (!filter.meta?.key || !indexPattern) { return true; } return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index 1e0957d816590..e33040485bf47 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -54,7 +54,7 @@ const translateToQuery = (filter: Filter) => { export const buildQueryFromFilters = ( filters: Filter[] = [], - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex: boolean = false ) => { filters = filters.filter(filter => filter && !isFilterDisabled(filter)); diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts index 000815b51f620..4574cd5ffd0cb 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts @@ -18,7 +18,7 @@ */ import { buildQueryFromKuery } from './from_kuery'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { fields } from '../../index_patterns/mocks'; import { Query } from '../../query/types'; @@ -30,7 +30,7 @@ describe('build query', () => { describe('buildQueryFromKuery', () => { test('should return the parameters of an Elasticsearch bool query', () => { - const result = buildQueryFromKuery(null, [], true); + const result = buildQueryFromKuery(undefined, [], true); const expected = { must: [], filter: [], diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts index f91c3d97b95b4..f4ec0fe0b34c5 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts @@ -17,12 +17,12 @@ * under the License. */ -import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { Query } from '../../query/types'; export function buildQueryFromKuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query[] = [], allowLeadingWildcards: boolean = false, dateFormatTZ?: string @@ -35,22 +35,20 @@ export function buildQueryFromKuery( } function buildQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queryASTs: KueryNode[], config: Record = {} ) { - const compoundQueryAST: KueryNode = nodeTypes.function.buildNode('and', queryASTs); - const kueryQuery: Record = toElasticsearchQuery( - compoundQueryAST, - indexPattern, - config - ); + const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); + const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); - return { - must: [], - filter: [], - should: [], - must_not: [], - ...kueryQuery.bool, - }; + return Object.assign( + { + must: [], + filter: [], + should: [], + must_not: [], + }, + kueryQuery.bool + ); } diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index 4617ee1a1c43d..e01240da87543 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -40,7 +40,7 @@ describe('migrateFilter', function() { } as unknown) as PhraseFilter; it('should migrate match filters of type phrase', function() { - const migratedFilter = migrateFilter(oldMatchPhraseFilter, null); + const migratedFilter = migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(migratedFilter, newMatchPhraseFilter)).toBe(true); }); @@ -48,7 +48,7 @@ describe('migrateFilter', function() { it('should not modify the original filter', function() { const oldMatchPhraseFilterCopy = clone(oldMatchPhraseFilter, true); - migrateFilter(oldMatchPhraseFilter, null); + migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(oldMatchPhraseFilter, oldMatchPhraseFilterCopy)).toBe(true); }); @@ -57,7 +57,7 @@ describe('migrateFilter', function() { const originalFilter = { match_all: {}, } as MatchAllFilter; - const migratedFilter = migrateFilter(originalFilter, null); + const migratedFilter = migrateFilter(originalFilter, undefined); expect(migratedFilter).toBe(originalFilter); expect(isEqual(migratedFilter, originalFilter)).toBe(true); diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index 258ab9e703131..fdc40768ebe41 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -43,7 +43,7 @@ function isMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter return Boolean(fieldName && get(filter, ['match', fieldName, 'type']) === 'phrase'); } -export function migrateFilter(filter: Filter, indexPattern: IIndexPattern | null) { +export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { if (isMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.match)[0]; const params: Record = get(filter, ['match', fieldName]); diff --git a/src/plugins/data/common/es_query/filters/build_filter.test.ts b/src/plugins/data/common/es_query/filters/build_filter.test.ts new file mode 100644 index 0000000000000..22b44035d6ca8 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/build_filter.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { buildFilter, FilterStateStore, FILTERS } from '.'; +import { stubIndexPattern, stubFields } from '../../../public/stubs'; + +describe('buildFilter', () => { + it('should build phrase filters', () => { + const params = 'foo'; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + stubIndexPattern, + stubFields[0], + FILTERS.PHRASE, + false, + false, + params, + alias, + state + ); + expect(filter.meta.negate).toBe(false); + expect(filter.meta.alias).toBe(alias); + + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } + }); + + it('should build phrases filters', () => { + const params = ['foo', 'bar']; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + stubIndexPattern, + stubFields[0], + FILTERS.PHRASES, + false, + false, + params, + alias, + state + ); + expect(filter.meta.type).toBe(FILTERS.PHRASES); + expect(filter.meta.negate).toBe(false); + expect(filter.meta.alias).toBe(alias); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } + }); + + it('should build range filters', () => { + const params = { from: 'foo', to: 'qux' }; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + stubIndexPattern, + stubFields[0], + FILTERS.RANGE, + false, + false, + params, + alias, + state + ); + expect(filter.meta.negate).toBe(false); + expect(filter.meta.alias).toBe(alias); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } + }); + + it('should build exists filters', () => { + const params = undefined; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + stubIndexPattern, + stubFields[0], + FILTERS.EXISTS, + false, + false, + params, + alias, + state + ); + expect(filter.meta.negate).toBe(false); + expect(filter.meta.alias).toBe(alias); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } + }); + + it('should include disabled state', () => { + const params = undefined; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + stubIndexPattern, + stubFields[0], + FILTERS.EXISTS, + true, + true, + params, + alias, + state + ); + expect(filter.meta.disabled).toBe(true); + expect(filter.meta.negate).toBe(true); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts new file mode 100644 index 0000000000000..affd213c29517 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/build_filters.ts @@ -0,0 +1,80 @@ +/* + * 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 { esFilters, IIndexPattern, IFieldType } from '../..'; +import { FilterMeta, FilterStateStore } from '.'; + +export function buildFilter( + indexPattern: IIndexPattern, + field: IFieldType, + type: esFilters.FILTERS, + negate: boolean, + disabled: boolean, + params: any, + alias: string | null, + store: esFilters.FilterStateStore +): esFilters.Filter { + const filter = buildBaseFilter(indexPattern, field, type, params); + filter.meta.alias = alias; + filter.meta.negate = negate; + filter.meta.disabled = disabled; + filter.$state = { store }; + return filter; +} + +export function buildCustomFilter( + indexPatternString: string, + queryDsl: any, + disabled: boolean, + negate: boolean, + alias: string | null, + store: FilterStateStore +): esFilters.Filter { + const meta: FilterMeta = { + index: indexPatternString, + type: esFilters.FILTERS.CUSTOM, + disabled, + negate, + alias, + }; + const filter: esFilters.Filter = { ...queryDsl, meta }; + filter.$state = { store }; + return filter; +} + +function buildBaseFilter( + indexPattern: IIndexPattern, + field: IFieldType, + type: esFilters.FILTERS, + params: any +): esFilters.Filter { + switch (type) { + case 'phrase': + return esFilters.buildPhraseFilter(field, params, indexPattern); + case 'phrases': + return esFilters.buildPhrasesFilter(field, params, indexPattern); + case 'range': + const newParams = { gte: params.from, lt: params.to }; + return esFilters.buildRangeFilter(field, newParams, indexPattern); + case 'exists': + return esFilters.buildExistsFilter(field, indexPattern); + default: + throw new Error(`Unknown filter type: ${type}`); + } +} diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.test.ts b/src/plugins/data/common/es_query/filters/get_filter_params.test.ts new file mode 100644 index 0000000000000..b0e992318327e --- /dev/null +++ b/src/plugins/data/common/es_query/filters/get_filter_params.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { phraseFilter, phrasesFilter, rangeFilter, existsFilter } from './stubs'; +import { getFilterParams } from './get_filter_params'; + +describe('getFilterParams', () => { + it('should retrieve params from phrase filter', () => { + const params = getFilterParams(phraseFilter); + expect(params).toBe('ios'); + }); + + it('should retrieve params from phrases filter', () => { + const params = getFilterParams(phrasesFilter); + expect(params).toEqual(['win xp', 'osx']); + }); + + it('should retrieve params from range filter', () => { + const params = getFilterParams(rangeFilter); + expect(params).toEqual({ from: 0, to: 10 }); + }); + + it('should return undefined for exists filter', () => { + const params = getFilterParams(existsFilter); + expect(params).toBeUndefined(); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.ts b/src/plugins/data/common/es_query/filters/get_filter_params.ts new file mode 100644 index 0000000000000..2e90ff0fe0691 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/get_filter_params.ts @@ -0,0 +1,34 @@ +/* + * 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 { Filter, FILTERS, PhraseFilter, PhrasesFilter, RangeFilter } from '.'; + +export function getFilterParams(filter: Filter) { + switch (filter.meta.type) { + case FILTERS.PHRASE: + return (filter as PhraseFilter).meta.params.query; + case FILTERS.PHRASES: + return (filter as PhrasesFilter).meta.params; + case FILTERS.RANGE: + return { + from: (filter as RangeFilter).meta.params.gte, + to: (filter as RangeFilter).meta.params.lt, + }; + } +} diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index c19545eb83a06..1bd534bf74ff7 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -20,6 +20,9 @@ import { omit, get } from 'lodash'; import { Filter } from './meta_filter'; +export * from './build_filters'; +export * from './get_filter_params'; + export * from './custom_filter'; export * from './exists_filter'; export * from './geo_bounding_box_filter'; diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index fa07b3e611fa7..3d819bd145fa6 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -63,18 +63,22 @@ export type RangeFilterMeta = FilterMeta & { formattedValue?: string; }; -export type RangeFilter = Filter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: string; - source: any; +export interface EsRangeFilter { + range: { [key: string]: RangeFilterParams }; +} + +export type RangeFilter = Filter & + EsRangeFilter & { + meta: RangeFilterMeta; + script?: { + script: { + params: any; + lang: string; + source: any; + }; }; + match_all?: any; }; - match_all?: any; - range: { [key: string]: RangeFilterParams }; -}; export const isRangeFilter = (filter: any): filter is RangeFilter => filter && filter.range; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/exists_filter.ts b/src/plugins/data/common/es_query/filters/stubs/exists_filter.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/exists_filter.ts rename to src/plugins/data/common/es_query/filters/stubs/exists_filter.ts index 5af97818f9bfb..13b8189b6e22f 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/exists_filter.ts +++ b/src/plugins/data/common/es_query/filters/stubs/exists_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters } from '../../../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; export const existsFilter: esFilters.ExistsFilter = { meta: { diff --git a/src/plugins/data/common/es_query/filters/stubs/index.ts b/src/plugins/data/common/es_query/filters/stubs/index.ts new file mode 100644 index 0000000000000..4f4c11f2e5ab7 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/stubs/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './exists_filter'; +export * from './phrase_filter'; +export * from './phrases_filter'; +export * from './range_filter'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts b/src/plugins/data/common/es_query/filters/stubs/phrase_filter.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts rename to src/plugins/data/common/es_query/filters/stubs/phrase_filter.ts index b6c8b9905e6b3..7456e056a02b1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts +++ b/src/plugins/data/common/es_query/filters/stubs/phrase_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters } from '../../../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; export const phraseFilter: esFilters.PhraseFilter = { meta: { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts b/src/plugins/data/common/es_query/filters/stubs/phrases_filter.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts rename to src/plugins/data/common/es_query/filters/stubs/phrases_filter.ts index 2e2ba4f798bdd..4bd70b85e186a 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/stubs/phrases_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters } from '../../../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; export const phrasesFilter: esFilters.PhrasesFilter = { meta: { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/range_filter.ts b/src/plugins/data/common/es_query/filters/stubs/range_filter.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/range_filter.ts rename to src/plugins/data/common/es_query/filters/stubs/range_filter.ts index c6438e30ecec6..5a6d245e2ef6f 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/fixtures/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/stubs/range_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters } from '../../../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; export const rangeFilter: esFilters.RangeFilter = { meta: { diff --git a/src/plugins/data/common/es_query/filters/types.ts b/src/plugins/data/common/es_query/filters/types.ts index a242df4811c05..01a921fc88ae7 100644 --- a/src/plugins/data/common/es_query/filters/types.ts +++ b/src/plugins/data/common/es_query/filters/types.ts @@ -48,4 +48,5 @@ export enum FILTERS { RANGE = 'range', GEO_BOUNDING_BOX = 'geo_bounding_box', GEO_POLYGON = 'geo_polygon', + SPATIAL_FILTER = 'spatial_filter', } diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 56eb45c4b1dca..937fe09903b6b 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -18,6 +18,7 @@ */ import * as esQuery from './es_query'; import * as esFilters from './filters'; +import * as esKuery from './kuery'; import * as utils from './utils'; -export { esFilters, esQuery, utils }; +export { esFilters, esQuery, utils, esKuery }; diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.js rename to src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts new file mode 100644 index 0000000000000..e441420760475 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -0,0 +1,421 @@ +/* + * 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 { + fromKueryExpression, + fromLiteralExpression, + toElasticsearchQuery, + doesKueryExpressionHaveLuceneSyntaxError, +} from './ast'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode } from '../types'; + +describe('kuery AST API', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('fromKueryExpression', () => { + test('should return a match all "is" function for whitespace', () => { + const expected = nodeTypes.function.buildNode('is', '*', '*'); + const actual = fromKueryExpression(' '); + expect(actual).toEqual(expected); + }); + + test('should return an "is" function with a null field for single literals', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression('foo'); + expect(actual).toEqual(expected); + }); + + test('should ignore extraneous whitespace at the beginning and end of the query', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression(' foo '); + expect(actual).toEqual(expected); + }); + + test('should not split on whitespace', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); + const actual = fromKueryExpression('foo bar'); + expect(actual).toEqual(expected); + }); + + test('should support "and" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo and bar'); + expect(actual).toEqual(expected); + }); + + test('should support "or" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo or bar'); + expect(actual).toEqual(expected); + }); + + test('should support negation of queries with a "not" prefix', () => { + const expected = nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]) + ); + const actual = fromKueryExpression('not (foo or bar)'); + expect(actual).toEqual(expected); + }); + + test('"and" should have a higher precedence than "or"', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'bar'), + nodeTypes.function.buildNode('is', null, 'baz'), + ]), + nodeTypes.function.buildNode('is', null, 'qux'), + ]), + ]); + const actual = fromKueryExpression('foo or bar and baz or qux'); + expect(actual).toEqual(expected); + }); + + test('should support grouping to override default precedence', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]), + nodeTypes.function.buildNode('is', null, 'baz'), + ]); + const actual = fromKueryExpression('(foo or bar) and baz'); + expect(actual).toEqual(expected); + }); + + test('should support matching against specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); + const actual = fromKueryExpression('foo:bar'); + expect(actual).toEqual(expected); + }); + + test('should also not split on whitespace when matching specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); + const actual = fromKueryExpression('foo:bar baz'); + expect(actual).toEqual(expected); + }); + + test('should treat quoted values as phrases', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); + const actual = fromKueryExpression('foo:"bar baz"'); + expect(actual).toEqual(expected); + }); + + test('should support a shorthand for matching multiple values against a single field', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]); + const actual = fromKueryExpression('foo:(bar or baz)'); + expect(actual).toEqual(expected); + }); + + test('should support "and" and "not" operators and grouping in the shorthand as well', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]), + nodeTypes.function.buildNode('not', nodeTypes.function.buildNode('is', 'foo', 'qux')), + ]); + const actual = fromKueryExpression('foo:((bar or baz) and not qux)'); + expect(actual).toEqual(expected); + }); + + test('should support exclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gt: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lt: 8000, + }), + ]); + const actual = fromKueryExpression('bytes > 1000 and bytes < 8000'); + expect(actual).toEqual(expected); + }); + + test('should support inclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gte: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lte: 8000, + }), + ]); + const actual = fromKueryExpression('bytes >= 1000 and bytes <= 8000'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in field names', () => { + const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); + const actual = fromKueryExpression('machine*:osx'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in values', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); + const actual = fromKueryExpression('foo:ba*'); + expect(actual).toEqual(expected); + }); + + test('should create an exists "is" query when a field is given and "*" is the value', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', '*'); + const actual = fromKueryExpression('foo:*'); + expect(actual).toEqual(expected); + }); + + test('should support nested queries indicated by curly braces', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ); + const actual = fromKueryExpression('nestedField:{ childOfNested: foo }'); + expect(actual).toEqual(expected); + }); + + test('should support nested subqueries and subqueries inside nested queries', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), + nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), + ]) + ), + ]); + const actual = fromKueryExpression( + 'response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested sub-queries inside paren groups', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'bar') + ), + ]), + ]); + const actual = fromKueryExpression( + 'response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested groups inside other nested groups', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode( + 'nested', + 'nestedChild', + nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') + ) + ); + const actual = fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); + expect(actual).toEqual(expected); + }); + }); + + describe('fromLiteralExpression', () => { + test('should create literal nodes for unquoted values with correct primitive types', () => { + const stringLiteral = nodeTypes.literal.buildNode('foo'); + const booleanFalseLiteral = nodeTypes.literal.buildNode(false); + const booleanTrueLiteral = nodeTypes.literal.buildNode(true); + const numberLiteral = nodeTypes.literal.buildNode(42); + + expect(fromLiteralExpression('foo')).toEqual(stringLiteral); + expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); + expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); + expect(fromLiteralExpression('42')).toEqual(numberLiteral); + }); + + test('should allow escaping of special characters with a backslash', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + // yo dawg + const actual = fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); + expect(actual).toEqual(expected); + }); + + test('should support double quoted strings that do not need escapes except for quotes', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + const actual = fromLiteralExpression('"\\():<>\\"*"'); + expect(actual).toEqual(expected); + }); + + test('should support escaped backslashes inside quoted strings', () => { + const expected = nodeTypes.literal.buildNode('\\'); + const actual = fromLiteralExpression('"\\\\"'); + expect(actual).toEqual(expected); + }); + + test('should detect wildcards and build wildcard AST nodes', () => { + const expected = nodeTypes.wildcard.buildNode('foo*bar'); + const actual = fromLiteralExpression('foo*bar'); + expect(actual).toEqual(expected); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given node type's ES query representation", () => { + const node = nodeTypes.function.buildNode('exists', 'response'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + expect(result).toEqual(expected); + }); + + test('should return an empty "and" function for undefined nodes and unknown node types', () => { + const expected = nodeTypes.function.toElasticsearchQuery( + nodeTypes.function.buildNode('and', []), + indexPattern + ); + + expect(toElasticsearchQuery((null as unknown) as KueryNode, undefined)).toEqual(expected); + + const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + delete noTypeNode.type; + expect(toElasticsearchQuery(noTypeNode)).toEqual(expected); + + const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + + // @ts-ignore + unknownTypeNode.type = 'notValid'; + expect(toElasticsearchQuery(unknownTypeNode)).toEqual(expected); + }); + + test("should return the given node type's ES query representation including a time zone parameter when one is provided", () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); + const result = toElasticsearchQuery(node, indexPattern, config); + expect(result).toEqual(expected); + }); + }); + + describe('doesKueryExpressionHaveLuceneSyntaxError', () => { + test('should return true for Lucene ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); + expect(result).toEqual(true); + }); + + test('should return false for KQL ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); + expect(result).toEqual(true); + }); + + test('should return false for KQL exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar:*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); + expect(result).toEqual(true); + }); + + test('should return false for KQL wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene regex', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene fuzziness', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene proximity', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene boosting', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene + operator', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene - operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene && operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene || operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for mixed KQL/Lucene queries', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); + expect(result).toEqual(true); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts new file mode 100644 index 0000000000000..253f432617972 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../node_types/index'; +import { KQLSyntaxError } from '../kuery_syntax_error'; +import { KueryNode, JsonObject, DslQuery, KueryParseOptions } from '../types'; +import { IIndexPattern } from '../../../index_patterns/types'; + +// @ts-ignore +import { parse as parseKuery } from './_generated_/kuery'; + +const fromExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {}, + parse: Function = parseKuery +): KueryNode => { + if (typeof expression === 'undefined') { + throw new Error('expression must be a string, got undefined instead'); + } + + return parse(expression, { ...parseOptions, helpers: { nodeTypes } }); +}; + +export const fromLiteralExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { + return fromExpression( + expression, + { + ...parseOptions, + startRule: 'Literal', + }, + parseKuery + ); +}; + +export const fromKueryExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { + try { + return fromExpression(expression, parseOptions, parseKuery); + } catch (error) { + if (error.name === 'SyntaxError') { + throw new KQLSyntaxError(error, expression); + } else { + throw error; + } + } +}; + +export const doesKueryExpressionHaveLuceneSyntaxError = ( + expression: string | DslQuery +): boolean => { + try { + fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); + return false; + } catch (e) { + return e.message.startsWith('Lucene'); + } +}; + +/** + * @params {String} indexPattern + * @params {Object} config - contains the dateFormatTZ + * + * IndexPattern isn't required, but if you pass one in, we can be more intelligent + * about how we craft the queries (e.g. scripted fields) + */ +export const toElasticsearchQuery = ( + node: KueryNode, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record +): JsonObject => { + if (!node || !node.type || !nodeTypes[node.type]) { + return toElasticsearchQuery(nodeTypes.function.buildNode('and', []), indexPattern); + } + + const nodeType = (nodeTypes[node.type] as unknown) as any; + + return nodeType.toElasticsearchQuery(node, indexPattern, config, context); +}; diff --git a/packages/kbn-es-query/src/kuery/ast/index.js b/src/plugins/data/common/es_query/kuery/ast/index.ts similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/index.js rename to src/plugins/data/common/es_query/kuery/ast/index.ts diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.peg rename to src/plugins/data/common/es_query/kuery/ast/kuery.peg diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/src/plugins/data/common/es_query/kuery/functions/and.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.js diff --git a/src/plugins/data/common/es_query/kuery/functions/and.test.ts b/src/plugins/data/common/es_query/kuery/functions/and.test.ts new file mode 100644 index 0000000000000..133e691b27dba --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/and.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import * as ast from '../ast'; + +// @ts-ignore +import * as and from './and'; + +const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); +const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + +describe('kuery functions', () => { + describe('and', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { + const result = and.buildNodeParams([childNode1, childNode2]); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's filter clause", () => { + const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]); + const result = and.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('filter'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.filter).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) + ); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.js diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.test.ts b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts new file mode 100644 index 0000000000000..8443436cf4cfb --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as exists from './exists'; + +describe('kuery functions', () => { + describe('exists', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return a single "arguments" param', () => { + const result = exists.buildNodeParams('response'); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const { + arguments: [arg], + } = exists.buildNodeParams('response'); + + expect(arg).toHaveProperty('type', 'literal'); + expect(arg).toHaveProperty('value', 'response'); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES exists query', () => { + const expected = { + exists: { field: 'response' }, + }; + const existsNode = nodeTypes.function.buildNode('exists', 'response'); + const result = exists.toElasticsearchQuery(existsNode, indexPattern); + + expect(expected).toEqual(result); + }); + + test('should return an ES exists query without an index pattern', () => { + const expected = { + exists: { field: 'response' }, + }; + const existsNode = nodeTypes.function.buildNode('exists', 'response'); + const result = exists.toElasticsearchQuery(existsNode); + + expect(expected).toEqual(result); + }); + + test('should throw an error for scripted fields', () => { + const existsNode = nodeTypes.function.buildNode('exists', 'script string'); + expect(() => exists.toElasticsearchQuery(existsNode, indexPattern)).toThrowError( + /Exists query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + exists: { field: 'nestedField.response' }, + }; + const existsNode = nodeTypes.function.buildNode('exists', 'response'); + const result = exists.toElasticsearchQuery( + existsNode, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(expected).toEqual(result); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js rename to src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts new file mode 100644 index 0000000000000..cf287ff2c437a --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoBoundingBox from './geo_bounding_box'; + +const params = { + bottomRight: { + lat: 50.73, + lon: -135.35, + }, + topLeft: { + lat: 73.12, + lon: -174.37, + }, +}; + +describe('kuery functions', () => { + describe('geoBoundingBox', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided params as named arguments with "lat, lon" string values', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [, ...args], + } = result; + + args.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['bottomRight', 'topLeft'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + + const { lat, lon } = get(params, param.name); + + expect(param.value.value).toBe(`${lat}, ${lon}`); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_bounding_box query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should return an ES geo_bounding_box query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_bounding_box.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); + + expect(() => geoBoundingBox.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo bounding box query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.js b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_polygon.js rename to src/plugins/data/common/es_query/kuery/functions/geo_polygon.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts new file mode 100644 index 0000000000000..84500cb4ade7e --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoPolygon from './geo_polygon'; + +const points = [ + { + lat: 69.77, + lon: -171.56, + }, + { + lat: 50.06, + lon: -169.1, + }, + { + lat: 69.16, + lon: -125.85, + }, +]; + +describe('kuery functions', () => { + describe('geoPolygon', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoPolygon.buildNodeParams('geo', points); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided points literal "lat, lon" string values', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [, ...args], + } = result; + + args.forEach((param: any, index: number) => { + const expectedPoint = points[index]; + const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; + + expect(param).toHaveProperty('type', 'literal'); + expect(param.value).toBe(expectedLatLon); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_polygon query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should return an ES geo_polygon query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_polygon.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); + expect(() => geoPolygon.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo polygon query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/index.js b/src/plugins/data/common/es_query/kuery/functions/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/index.js rename to src/plugins/data/common/es_query/kuery/functions/index.js diff --git a/src/plugins/data/common/es_query/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js new file mode 100644 index 0000000000000..4f2f298c4707d --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -0,0 +1,173 @@ +/* + * 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 { get, isUndefined } from 'lodash'; +import { getPhraseScript } from '../../filters'; +import { getFields } from './utils/get_fields'; +import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getFullFieldNameNode } from './utils/get_full_field_name_node'; + +import * as ast from '../ast'; + +import * as literal from '../node_types/literal'; +import * as wildcard from '../node_types/wildcard'; + +export function buildNodeParams(fieldName, value, isPhrase = false) { + if (isUndefined(fieldName)) { + throw new Error('fieldName is a required argument'); + } + if (isUndefined(value)) { + throw new Error('value is a required argument'); + } + const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); + const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value); + const isPhraseNode = literal.buildNode(isPhrase); + return { + arguments: [fieldNode, valueNode, isPhraseNode], + }; +} + +export function toElasticsearchQuery(node, indexPattern = null, config = {}, context = {}) { + const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; + const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); + const fieldName = ast.toElasticsearchQuery(fullFieldNameArg); + const value = !isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; + const type = isPhraseArg.value ? 'phrase' : 'best_fields'; + if (fullFieldNameArg.value === null) { + if (valueArg.type === 'wildcard') { + return { + query_string: { + query: wildcard.toQueryStringQuery(valueArg), + }, + }; + } + + return { + multi_match: { + type, + query: value, + lenient: true, + } + }; + } + + const fields = indexPattern ? getFields(fullFieldNameArg, indexPattern) : []; + // If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve + // the behaviour of lucene on dashboards where there are panels based on different index patterns that have different + // fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the + // field should return no results. It's debatable whether this is desirable, but it's been that way forever, so we'll + // keep things familiar for now. + if (fields && fields.length === 0) { + fields.push({ + name: ast.toElasticsearchQuery(fullFieldNameArg), + scripted: false, + }); + } + + const isExistsQuery = valueArg.type === 'wildcard' && value === '*'; + const isAllFieldsQuery = + (fullFieldNameArg.type === 'wildcard' && fieldName === '*') + || (fields && indexPattern && fields.length === indexPattern.fields.length); + const isMatchAllQuery = isExistsQuery && isAllFieldsQuery; + + if (isMatchAllQuery) { + return { match_all: {} }; + } + + const queries = fields.reduce((accumulator, field) => { + const wrapWithNestedQuery = (query) => { + // Wildcards can easily include nested and non-nested fields. There isn't a good way to let + // users handle this themselves so we automatically add nested queries in this scenario. + if ( + !(fullFieldNameArg.type === 'wildcard') + || !get(field, 'subType.nested') + || context.nested + ) { + return query; + } + else { + return { + nested: { + path: field.subType.nested.path, + query, + score_mode: 'none' + } + }; + } + }; + + if (field.scripted) { + // Exists queries don't make sense for scripted fields + if (!isExistsQuery) { + return [...accumulator, { + script: { + ...getPhraseScript(field, value) + } + }]; + } + } + else if (isExistsQuery) { + return [...accumulator, wrapWithNestedQuery({ + exists: { + field: field.name + } + })]; + } + else if (valueArg.type === 'wildcard') { + return [...accumulator, wrapWithNestedQuery({ + query_string: { + fields: [field.name], + query: wildcard.toQueryStringQuery(valueArg), + } + })]; + } + /* + If we detect that it's a date field and the user wants an exact date, we need to convert the query to both >= and <= the value provided to force a range query. This is because match and match_phrase queries do not accept a timezone parameter. + dateFormatTZ can have the value of 'Browser', in which case we guess the timezone using moment.tz.guess. + */ + else if (field.type === 'date') { + const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; + return [...accumulator, wrapWithNestedQuery({ + range: { + [field.name]: { + gte: value, + lte: value, + ...timeZoneParam, + }, + } + })]; + } + else { + const queryType = type === 'phrase' ? 'match_phrase' : 'match'; + return [...accumulator, wrapWithNestedQuery({ + [queryType]: { + [field.name]: value + } + })]; + } + }, []); + + return { + bool: { + should: queries, + minimum_should_match: 1 + } + }; +} + diff --git a/src/plugins/data/common/es_query/kuery/functions/is.test.ts b/src/plugins/data/common/es_query/kuery/functions/is.test.ts new file mode 100644 index 0000000000000..df147bad54a34 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/is.test.ts @@ -0,0 +1,305 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; + +// @ts-ignore +import * as is from './is'; +import { IIndexPattern } from '../../../index_patterns'; + +describe('kuery functions', () => { + describe('is', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('fieldName and value should be required arguments', () => { + expect(() => is.buildNodeParams()).toThrowError(/fieldName is a required argument/); + expect(() => is.buildNodeParams('foo')).toThrowError(/value is a required argument/); + }); + + test('arguments should contain the provided fieldName and value as literals', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('response', 200); + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'response'); + expect(value).toHaveProperty('type', 'literal'); + expect(value).toHaveProperty('value', 200); + }); + + test('should detect wildcards in the provided arguments', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('machine*', 'win*'); + + expect(fieldName).toHaveProperty('type', 'wildcard'); + expect(value).toHaveProperty('type', 'wildcard'); + }); + + test('should default to a non-phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200); + expect(isPhrase.value).toBe(false); + }); + + test('should allow specification of a phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200, true); + expect(isPhrase.value).toBe(true); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES match_all query when fieldName and value are both "*"', () => { + const expected = { + match_all: {}, + }; + const node = nodeTypes.function.buildNode('is', '*', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES multi_match query using default_field when fieldName is null', () => { + const expected = { + multi_match: { + query: 200, + type: 'best_fields', + lenient: true, + }, + }; + const node = nodeTypes.function.buildNode('is', null, 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', () => { + const expected = { + query_string: { + query: 'jpg*', + }, + }; + const node = nodeTypes.function.buildNode('is', null, 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES bool query with a sub-query for each field when fieldName is "*"', () => { + const node = nodeTypes.function.buildNode('is', '*', 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(result.bool.should.length).toBe(indexPattern.fields.length); + }); + + test('should return an ES exists query when value is "*"', () => { + const expected = { + bool: { + should: [{ exists: { field: 'extension' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided without an index pattern', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node); + + expect(result).toEqual(expected); + }); + + test('should support creation of phrase queries', () => { + const expected = { + bool: { + should: [{ match_phrase: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should create a query_string query for wildcard values', () => { + const expected = { + bool: { + should: [ + { + query_string: { + fields: ['extension'], + query: 'jpg*', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support scripted fields', () => { + const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result.bool.should[0]).toHaveProperty('script'); + }); + + test('should support date fields without a dateFormat provided', () => { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support date fields with a dateFormat provided', () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern, config); + + expect(result).toEqual(expected); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + bool: { + should: [{ match: { 'nestedField.extension': 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toEqual(expected); + }); + + test('should support wildcard field names', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { + const expected = { + bool: { + should: [ + { + nested: { + path: 'nestedField.nestedChild', + query: { + match: { + 'nestedField.nestedChild.doublyNestedChild': 'foo', + }, + }, + score_mode: 'none', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.js diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.test.ts b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts new file mode 100644 index 0000000000000..945a36d304a05 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +import * as ast from '../ast'; + +// @ts-ignore +import * as nested from './nested'; + +const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); + +describe('kuery functions', () => { + describe('nested', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { + const result = nested.buildNodeParams('nestedField', childNode); + const { + arguments: [resultPath, resultChildNode], + } = result; + + expect(ast.toElasticsearchQuery(resultPath)).toBe('nestedField'); + expect(resultChildNode).toBe(childNode); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should wrap subqueries in an ES nested query', () => { + const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); + const result = nested.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('nested'); + expect(Object.keys(result).length).toBe(1); + + expect(result.nested.path).toBe('nestedField'); + expect(result.nested.score_mode).toBe('none'); + }); + + test('should pass the nested path to subqueries so the full field name can be used', () => { + const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); + const result = nested.toElasticsearchQuery(node, indexPattern); + const expectedSubQuery = ast.toElasticsearchQuery( + nodeTypes.function.buildNode('is', 'nestedField.child', 'foo') + ); + + expect(result.nested.query).toEqual(expectedSubQuery); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/src/plugins/data/common/es_query/kuery/functions/not.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.js diff --git a/src/plugins/data/common/es_query/kuery/functions/not.test.ts b/src/plugins/data/common/es_query/kuery/functions/not.test.ts new file mode 100644 index 0000000000000..01c1976b939ea --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/not.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +import * as ast from '../ast'; + +// @ts-ignore +import * as not from './not'; + +const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + +describe('kuery functions', () => { + describe('not', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child node', () => { + const { + arguments: [actualChild], + } = not.buildNodeParams(childNode); + + expect(actualChild).toBe(childNode); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should wrap a subquery in an ES bool query's must_not clause", () => { + const node = nodeTypes.function.buildNode('not', childNode); + const result = not.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + + expect(result.bool).toHaveProperty('must_not'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.must_not).toEqual(ast.toElasticsearchQuery(childNode, indexPattern)); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/src/plugins/data/common/es_query/kuery/functions/or.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.js diff --git a/src/plugins/data/common/es_query/kuery/functions/or.test.ts b/src/plugins/data/common/es_query/kuery/functions/or.test.ts new file mode 100644 index 0000000000000..a6590546e5fc5 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/or.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +import * as ast from '../ast'; + +// @ts-ignore +import * as or from './or'; + +const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); +const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + +describe('kuery functions', () => { + describe('or', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { + const result = or.buildNodeParams([childNode1, childNode2]); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's should clause", () => { + const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); + const result = or.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('should'); + expect(result.bool.should).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) + ); + }); + + test('should require one of the clauses to match', () => { + const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); + const result = or.toElasticsearchQuery(node, indexPattern); + + expect(result.bool).toHaveProperty('minimum_should_match', 1); + }); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js new file mode 100644 index 0000000000000..80181cfc003f1 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -0,0 +1,130 @@ +/* + * 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 _ from 'lodash'; +import { nodeTypes } from '../node_types'; +import * as ast from '../ast'; +import { getRangeScript } from '../../filters'; +import { getFields } from './utils/get_fields'; +import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getFullFieldNameNode } from './utils/get_full_field_name_node'; + +export function buildNodeParams(fieldName, params) { + params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format'); + const fieldNameArg = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : nodeTypes.literal.buildNode(fieldName); + const args = _.map(params, (value, key) => { + return nodeTypes.namedArg.buildNode(key, value); + }); + + return { + arguments: [fieldNameArg, ...args], + }; +} + +export function toElasticsearchQuery(node, indexPattern = null, config = {}, context = {}) { + const [ fieldNameArg, ...args ] = node.arguments; + const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); + const fields = indexPattern ? getFields(fullFieldNameArg, indexPattern) : []; + const namedArgs = extractArguments(args); + const queryParams = _.mapValues(namedArgs, ast.toElasticsearchQuery); + + // If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve + // the behaviour of lucene on dashboards where there are panels based on different index patterns that have different + // fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the + // field should return no results. It's debatable whether this is desirable, but it's been that way forever, so we'll + // keep things familiar for now. + if (fields && fields.length === 0) { + fields.push({ + name: ast.toElasticsearchQuery(fullFieldNameArg), + scripted: false, + }); + } + + + const queries = fields.map((field) => { + const wrapWithNestedQuery = (query) => { + // Wildcards can easily include nested and non-nested fields. There isn't a good way to let + // users handle this themselves so we automatically add nested queries in this scenario. + if ( + !fullFieldNameArg.type === 'wildcard' + || !_.get(field, 'subType.nested') + || context.nested + ) { + return query; + } + else { + return { + nested: { + path: field.subType.nested.path, + query, + score_mode: 'none' + } + }; + } + }; + + if (field.scripted) { + return { + script: getRangeScript(field, queryParams), + }; + } + else if (field.type === 'date') { + const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {}; + return wrapWithNestedQuery({ + range: { + [field.name]: { + ...queryParams, + ...timeZoneParam, + } + } + }); + } + return wrapWithNestedQuery({ + range: { + [field.name]: queryParams + } + }); + }); + + return { + bool: { + should: queries, + minimum_should_match: 1 + } + }; +} + +function extractArguments(args) { + if ((args.gt && args.gte) || (args.lt && args.lte)) { + throw new Error('range ends cannot be both inclusive and exclusive'); + } + + const unnamedArgOrder = ['gte', 'lte', 'format']; + + return args.reduce((acc, arg, index) => { + if (arg.type === 'namedArg') { + acc[arg.name] = arg.value; + } + else { + acc[unnamedArgOrder[index]] = arg; + } + + return acc; + }, {}); +} diff --git a/src/plugins/data/common/es_query/kuery/functions/range.test.ts b/src/plugins/data/common/es_query/kuery/functions/range.test.ts new file mode 100644 index 0000000000000..ed8e40830df02 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/range.test.ts @@ -0,0 +1,256 @@ +/* + * 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 { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { RangeFilterParams } from '../../filters'; + +// @ts-ignore +import * as range from './range'; + +describe('kuery functions', () => { + describe('range', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('arguments should contain the provided fieldName as a literal', () => { + const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 }); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'bytes'); + }); + + test('arguments should contain the provided params as named arguments', () => { + const givenParams: RangeFilterParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; + const result = range.buildNodeParams('bytes', givenParams); + const { + arguments: [, ...params], + } = result; + + expect(Array.isArray(params)).toBeTruthy(); + expect(params.length).toBeGreaterThan(1); + + params.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['gt', 'lt', 'format'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + expect(param.value.value).toBe(get(givenParams, param.name)); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return an ES range query for the node's field and params", () => { + const expected = { + bool: { + should: [ + { + range: { + bytes: { + gt: 1000, + lt: 8000, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); + const result = range.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES range query without an index pattern', () => { + const expected = { + bool: { + should: [ + { + range: { + bytes: { + gt: 1000, + lt: 8000, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + + const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); + const result = range.toElasticsearchQuery(node); + + expect(result).toEqual(expected); + }); + + test('should support wildcard field names', () => { + const expected = { + bool: { + should: [ + { + range: { + bytes: { + gt: 1000, + lt: 8000, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + + const node = nodeTypes.function.buildNode('range', 'byt*', { gt: 1000, lt: 8000 }); + const result = range.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support scripted fields', () => { + const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 }); + const result = range.toElasticsearchQuery(node, indexPattern); + + expect(result.bool.should[0]).toHaveProperty('script'); + }); + + test('should support date fields without a dateFormat provided', () => { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); + const result = range.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support date fields with a dateFormat provided', () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); + const result = range.toElasticsearchQuery(node, indexPattern, config); + + expect(result).toEqual(expected); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + bool: { + should: [ + { + range: { + 'nestedField.bytes': { + gt: 1000, + lt: 8000, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); + const result = range.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toEqual(expected); + }); + + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { + const expected = { + bool: { + should: [ + { + nested: { + path: 'nestedField.nestedChild', + query: { + range: { + 'nestedField.nestedChild.doublyNestedChild': { + gt: 1000, + lt: 8000, + }, + }, + }, + score_mode: 'none', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('range', '*doublyNested*', { + gt: 1000, + lt: 8000, + }); + const result = range.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts new file mode 100644 index 0000000000000..d48f0943082c9 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { fields } from '../../../../index_patterns/mocks'; + +import { nodeTypes } from '../../index'; +import { IIndexPattern, IFieldType } from '../../../../index_patterns'; + +// @ts-ignore +import { getFields } from './get_fields'; + +describe('getFields', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('field names without a wildcard', () => { + test('should return an empty array if the field does not exist in the index pattern', () => { + const fieldNameNode = nodeTypes.literal.buildNode('nonExistentField'); + const actual = getFields(fieldNameNode, indexPattern); + + expect(actual).toEqual([]); + }); + + test('should return the single matching field in an array', () => { + const fieldNameNode = nodeTypes.literal.buildNode('extension'); + const results = getFields(fieldNameNode, indexPattern); + + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('extension'); + }); + + test('should not match a wildcard in a literal node', () => { + const indexPatternWithWildField = { + title: 'wildIndex', + fields: [ + { + name: 'foo*', + }, + ], + }; + + const fieldNameNode = nodeTypes.literal.buildNode('foo*'); + const results = getFields(fieldNameNode, indexPatternWithWildField); + + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('foo*'); + + const actual = getFields(nodeTypes.literal.buildNode('fo*'), indexPatternWithWildField); + expect(actual).toEqual([]); + }); + }); + + describe('field name patterns with a wildcard', () => { + test('should return an empty array if test does not match any fields in the index pattern', () => { + const fieldNameNode = nodeTypes.wildcard.buildNode('nonExistent*'); + const actual = getFields(fieldNameNode, indexPattern); + + expect(actual).toEqual([]); + }); + + test('should return all fields that match the pattern in an array', () => { + const fieldNameNode = nodeTypes.wildcard.buildNode('machine*'); + const results = getFields(fieldNameNode, indexPattern); + + expect(Array.isArray(results)).toBeTruthy(); + expect(results).toHaveLength(2); + expect(results.find((field: IFieldType) => field.name === 'machine.os')).toBeDefined(); + expect(results.find((field: IFieldType) => field.name === 'machine.os.raw')).toBeDefined(); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts new file mode 100644 index 0000000000000..e138e22b76ad3 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { nodeTypes } from '../../node_types'; +import { fields } from '../../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../../index_patterns'; + +// @ts-ignore +import { getFullFieldNameNode } from './get_full_field_name_node'; + +describe('getFullFieldNameNode', function() { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + test('should return unchanged name node if no nested path is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('notNested'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should add the nested path if test is valid according to the index pattern', () => { + const nameNode = nodeTypes.literal.buildNode('child'); + const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); + + expect(result).toEqual(nodeTypes.literal.buildNode('nestedField.child')); + }); + + test('should throw an error if a path is provided for a non-nested field', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'machine')).toThrowError( + /machine.os is not a nested field but is in nested group "machine" in the KQL expression/ + ); + }); + + test('should throw an error if a nested field is not passed with a path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedField.child'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern)).toThrowError( + /nestedField.child is a nested field, but is not in a nested group in the KQL expression./ + ); + }); + + test('should throw an error if a nested field is passed with the wrong path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'nestedField')).toThrowError( + /Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/ + ); + }); + + test('should skip error checking for wildcard names', () => { + const nameNode = nodeTypes.wildcard.buildNode('nested*'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should skip error checking if no index pattern is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, null, 'machine')).not.toThrowError(); + + const result = getFullFieldNameNode(nameNode, null, 'machine'); + expect(result).toEqual(nodeTypes.literal.buildNode('machine.os')); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/index.ts b/src/plugins/data/common/es_query/kuery/index.ts new file mode 100644 index 0000000000000..4184dea62ef2c --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { KQLSyntaxError } from './kuery_syntax_error'; +export { nodeTypes } from './node_types'; +export * from './ast'; + +export * from './types'; diff --git a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts new file mode 100644 index 0000000000000..cfe2f86e813ca --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { fromKueryExpression } from './ast'; + +describe('kql syntax errors', () => { + it('should throw an error for a field query missing a value', () => { + expect(() => { + fromKueryExpression('response:'); + }).toThrow( + 'Expected "(", "{", value, whitespace but end of input found.\n' + + 'response:\n' + + '---------^' + ); + }); + + it('should throw an error for an OR query missing a right side sub-query', () => { + expect(() => { + fromKueryExpression('response:200 or '); + }).toThrow( + 'Expected "(", NOT, field name, value but end of input found.\n' + + 'response:200 or \n' + + '----------------^' + ); + }); + + it('should throw an error for an OR list of values missing a right side sub-query', () => { + expect(() => { + fromKueryExpression('response:(200 or )'); + }).toThrow( + 'Expected "(", NOT, value but ")" found.\n' + 'response:(200 or )\n' + '-----------------^' + ); + }); + + it('should throw an error for a NOT query missing a sub-query', () => { + expect(() => { + fromKueryExpression('response:200 and not '); + }).toThrow( + 'Expected "(", field name, value but end of input found.\n' + + 'response:200 and not \n' + + '---------------------^' + ); + }); + + it('should throw an error for a NOT list missing a sub-query', () => { + expect(() => { + fromKueryExpression('response:(200 and not )'); + }).toThrow( + 'Expected "(", value but ")" found.\n' + + 'response:(200 and not )\n' + + '----------------------^' + ); + }); + + it('should throw an error for unbalanced quotes', () => { + expect(() => { + fromKueryExpression('foo:"ba '); + }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + 'foo:"ba \n' + '----^'); + }); + + it('should throw an error for unescaped quotes in a quoted string', () => { + expect(() => { + fromKueryExpression('foo:"ba "r"'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but "r" found.\n' + 'foo:"ba "r"\n' + '---------^' + ); + }); + + it('should throw an error for unescaped special characters in literals', () => { + expect(() => { + fromKueryExpression('foo:ba:r'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but ":" found.\n' + 'foo:ba:r\n' + '------^' + ); + }); + + it('should throw an error for range queries missing a value', () => { + expect(() => { + fromKueryExpression('foo > '); + }).toThrow('Expected literal, whitespace but end of input found.\n' + 'foo > \n' + '------^'); + }); + + it('should throw an error for range queries missing a field', () => { + expect(() => { + fromKueryExpression('< 1000'); + }).toThrow( + 'Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + + '< 1000\n' + + '^' + ); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts new file mode 100644 index 0000000000000..7c90119fcc1bc --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts @@ -0,0 +1,78 @@ +/* + * 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 { repeat } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +const endOfInputText = i18n.translate('data.common.esQuery.kql.errors.endOfInputText', { + defaultMessage: 'end of input', +}); + +const grammarRuleTranslations: Record = { + fieldName: i18n.translate('data.common.esQuery.kql.errors.fieldNameText', { + defaultMessage: 'field name', + }), + value: i18n.translate('data.common.esQuery.kql.errors.valueText', { + defaultMessage: 'value', + }), + literal: i18n.translate('data.common.esQuery.kql.errors.literalText', { + defaultMessage: 'literal', + }), + whitespace: i18n.translate('data.common.esQuery.kql.errors.whitespaceText', { + defaultMessage: 'whitespace', + }), +}; + +interface KQLSyntaxErrorData extends Error { + found: string; + expected: KQLSyntaxErrorExpected[]; + location: any; +} + +interface KQLSyntaxErrorExpected { + description: string; +} + +export class KQLSyntaxError extends Error { + shortMessage: string; + + constructor(error: KQLSyntaxErrorData, expression: any) { + const translatedExpectations = error.expected.map(expected => { + return grammarRuleTranslations[expected.description] || expected.description; + }); + + const translatedExpectationText = translatedExpectations.join(', '); + + const message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { + defaultMessage: 'Expected {expectedList} but {foundInput} found.', + values: { + expectedList: translatedExpectationText, + foundInput: error.found ? `"${error.found}"` : endOfInputText, + }, + }); + + const fullMessage = [message, expression, repeat('-', error.location.start.offset) + '^'].join( + '\n' + ); + + super(fullMessage); + this.name = 'KQLSyntaxError'; + this.shortMessage = message; + } +} diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/src/plugins/data/common/es_query/kuery/node_types/function.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/function.js rename to src/plugins/data/common/es_query/kuery/node_types/function.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts new file mode 100644 index 0000000000000..ca9798eb6e74f --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { fields } from '../../../index_patterns/mocks'; + +import { nodeTypes } from './index'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; +// @ts-ignore +import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; + +describe('kuery node types', () => { + describe('function', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNode', () => { + test('should return a node representing the given kuery function', () => { + const result = buildNode('is', 'extension', 'jpg'); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + }); + }); + + describe('buildNodeWithArgumentNodes', () => { + test('should return a function node with the given argument list untouched', () => { + const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); + const valueLiteral = nodeTypes.literal.buildNode('jpg'); + const argumentNodes = [fieldNameLiteral, valueLiteral]; + const result = buildNodeWithArgumentNodes('is', argumentNodes); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + expect(result.arguments).toBe(argumentNodes); + expect(result.arguments).toEqual(argumentNodes); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given function type's ES query representation", () => { + const node = buildNode('is', 'extension', 'jpg'); + const expected = isFunctionToElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + + expect(expected).toEqual(result); + }); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/node_types/index.d.ts b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts new file mode 100644 index 0000000000000..720d64e11a0f8 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts @@ -0,0 +1,97 @@ +/* + * 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. + */ + +/** + * WARNING: these typings are incomplete + */ + +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode, JsonValue } from '..'; + +type FunctionName = + | 'is' + | 'and' + | 'or' + | 'not' + | 'range' + | 'exists' + | 'geoBoundingBox' + | 'geoPolygon' + | 'nested'; + +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: ( + node: any, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record + ) => JsonValue; +} + +interface FunctionTypeBuildNode { + type: 'function'; + function: FunctionName; + // TODO -> Need to define a better type for DSL query + arguments: any[]; +} + +interface LiteralType { + buildNode: (value: null | boolean | number | string) => LiteralTypeBuildNode; + toElasticsearchQuery: (node: any) => null | boolean | number | string; +} + +interface LiteralTypeBuildNode { + type: 'literal'; + value: null | boolean | number | string; +} + +interface NamedArgType { + buildNode: (name: string, value: any) => NamedArgTypeBuildNode; + toElasticsearchQuery: (node: any) => string; +} + +interface NamedArgTypeBuildNode { + type: 'namedArg'; + name: string; + value: any; +} + +interface WildcardType { + buildNode: (value: string) => WildcardTypeBuildNode; + test: (node: any, string: string) => boolean; + toElasticsearchQuery: (node: any) => string; + toQueryStringQuery: (node: any) => string; + hasLeadingWildcard: (node: any) => boolean; +} + +interface WildcardTypeBuildNode { + type: 'wildcard'; + value: string; +} + +interface NodeTypes { + function: FunctionType; + literal: LiteralType; + namedArg: NamedArgType; + wildcard: WildcardType; +} + +export const nodeTypes: NodeTypes; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.js b/src/plugins/data/common/es_query/kuery/node_types/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/index.js rename to src/plugins/data/common/es_query/kuery/node_types/index.js diff --git a/packages/kbn-es-query/src/kuery/node_types/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts new file mode 100644 index 0000000000000..60fe2d6d1013c --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './literal'; + +describe('kuery node types', () => { + describe('literal', () => { + describe('buildNode', () => { + test('should return a node representing the given value', () => { + const result = buildNode('foo'); + + expect(result).toHaveProperty('type', 'literal'); + expect(result).toHaveProperty('value', 'foo'); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the literal value represented by the given node', () => { + const node = buildNode('foo'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo'); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.js b/src/plugins/data/common/es_query/kuery/node_types/named_arg.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/named_arg.js rename to src/plugins/data/common/es_query/kuery/node_types/named_arg.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts new file mode 100644 index 0000000000000..36c40d28e55c2 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { nodeTypes } from './index'; + +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './named_arg'; + +describe('kuery node types', () => { + describe('named arg', () => { + describe('buildNode', () => { + test('should return a node representing a named argument with the given value', () => { + const result = buildNode('fieldName', 'foo'); + expect(result).toHaveProperty('type', 'namedArg'); + expect(result).toHaveProperty('name', 'fieldName'); + expect(result).toHaveProperty('value'); + + const literalValue = result.value; + expect(literalValue).toHaveProperty('type', 'literal'); + expect(literalValue).toHaveProperty('value', 'foo'); + }); + + test('should support literal nodes as values', () => { + const value = nodeTypes.literal.buildNode('foo'); + const result = buildNode('fieldName', value); + + expect(result.value).toBe(value); + expect(result.value).toEqual(value); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the argument value represented by the given node', () => { + const node = buildNode('fieldName', 'foo'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo'); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/wildcard.js b/src/plugins/data/common/es_query/kuery/node_types/wildcard.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/wildcard.js rename to src/plugins/data/common/es_query/kuery/node_types/wildcard.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts new file mode 100644 index 0000000000000..7e221d96b49e9 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + buildNode, + wildcardSymbol, + hasLeadingWildcard, + toElasticsearchQuery, + test as testNode, + toQueryStringQuery, + // @ts-ignore +} from './wildcard'; + +describe('kuery node types', () => { + describe('wildcard', () => { + describe('buildNode', () => { + test('should accept a string argument representing a wildcard string', () => { + const wildcardValue = `foo${wildcardSymbol}bar`; + const result = buildNode(wildcardValue); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result).toHaveProperty('value', wildcardValue); + }); + + test('should accept and parse a wildcard string', () => { + const result = buildNode('foo*bar'); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result.value).toBe(`foo${wildcardSymbol}bar`); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo*bar'); + }); + }); + + describe('toQueryStringQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('foo*bar'); + }); + + test('should escape query_string query special characters other than wildcard', () => { + const node = buildNode('+foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('\\+foo*bar'); + }); + }); + + describe('test', () => { + test('should return a boolean indicating whether the string matches the given wildcard node', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'foobazbar')).toBe(true); + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'fooqux')).toBe(false); + expect(testNode(node, 'bazbar')).toBe(false); + }); + + test('should return a true even when the string has newlines or tabs', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foo\nbar')).toBe(true); + expect(testNode(node, 'foo\tbar')).toBe(true); + }); + }); + + describe('hasLeadingWildcard', () => { + test('should determine whether a wildcard node contains a leading wildcard', () => { + const node = buildNode('foo*bar'); + expect(hasLeadingWildcard(node)).toBe(false); + + const leadingWildcardNode = buildNode('*foobar'); + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(true); + }); + + // Lone wildcards become exists queries, so we aren't worried about their performance + test('should not consider a lone wildcard to be a leading wildcard', () => { + const leadingWildcardNode = buildNode('*'); + + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(false); + }); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/kuery/types.ts b/src/plugins/data/common/es_query/kuery/types.ts new file mode 100644 index 0000000000000..86cb7e08a767c --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -0,0 +1,45 @@ +/* + * 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 { NodeTypes } from './node_types'; + +export interface KueryNode { + type: keyof NodeTypes; + [key: string]: any; +} + +export type DslQuery = any; + +export interface KueryParseOptions { + helpers: { + [key: string]: any; + }; + startRule: string; + allowLeadingWildcards: boolean; + errorOnLuceneSyntax: boolean; +} + +export { nodeTypes } from './node_types'; + +export type JsonArray = JsonValue[]; +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} diff --git a/src/plugins/data/common/es_query/utils/get_display_value.ts b/src/plugins/data/common/es_query/utils/get_display_value.ts new file mode 100644 index 0000000000000..4bf7e1c9c6ba7 --- /dev/null +++ b/src/plugins/data/common/es_query/utils/get_display_value.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 { get } from 'lodash'; +import { IIndexPattern, IFieldType } from '../..'; +import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; +import { Filter } from '../filters'; + +function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { + if (!indexPattern || !key) return; + let format = get(indexPattern, ['fields', 'byName', key, 'format']); + if (!format && (indexPattern.fields as any).getByName) { + // TODO: Why is indexPatterns sometimes a map and sometimes an array? + format = ((indexPattern.fields as any).getByName(key) as IFieldType).format; + } + return format; +} + +export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexPattern[]): string { + const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); + + if (typeof filter.meta.value === 'function') { + const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key); + return filter.meta.value(valueFormatter); + } else { + return filter.meta.value || ''; + } +} diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts b/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts new file mode 100644 index 0000000000000..2f31fafcb74e4 --- /dev/null +++ b/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { stubIndexPattern, phraseFilter } from 'src/plugins/data/public/stubs'; +import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; + +describe('getIndexPatternFromFilter', () => { + it('should return the index pattern from the filter', () => { + const indexPattern = getIndexPatternFromFilter(phraseFilter, [stubIndexPattern]); + expect(indexPattern).toBe(stubIndexPattern); + }); +}); diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts b/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts new file mode 100644 index 0000000000000..43d4bdaf03bc1 --- /dev/null +++ b/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts @@ -0,0 +1,28 @@ +/* + * 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 { Filter } from '../filters'; +import { IIndexPattern } from '../..'; + +export function getIndexPatternFromFilter( + filter: Filter, + indexPatterns: IIndexPattern[] +): IIndexPattern | undefined { + return indexPatterns.find(indexPattern => indexPattern.id === filter.meta.index); +} diff --git a/src/plugins/data/common/es_query/utils/index.ts b/src/plugins/data/common/es_query/utils/index.ts index 27f51c1f44cf2..79856c9e0267e 100644 --- a/src/plugins/data/common/es_query/utils/index.ts +++ b/src/plugins/data/common/es_query/utils/index.ts @@ -18,3 +18,5 @@ */ export * from './get_time_zone_from_settings'; +export * from './get_index_pattern_from_filter'; +export * from './get_display_value'; diff --git a/src/plugins/data/common/field_formats/converters/boolean.test.ts b/src/plugins/data/common/field_formats/converters/boolean.test.ts index 2a548a6c1b179..3650df6517611 100644 --- a/src/plugins/data/common/field_formats/converters/boolean.test.ts +++ b/src/plugins/data/common/field_formats/converters/boolean.test.ts @@ -23,7 +23,7 @@ describe('Boolean Format', () => { let boolean: Record; beforeEach(() => { - boolean = new BoolFormat(); + boolean = new BoolFormat({}, jest.fn()); }); [ diff --git a/src/plugins/data/common/field_formats/converters/boolean.ts b/src/plugins/data/common/field_formats/converters/boolean.ts index 96e353592d676..6cc6c71465d50 100644 --- a/src/plugins/data/common/field_formats/converters/boolean.ts +++ b/src/plugins/data/common/field_formats/converters/boolean.ts @@ -19,11 +19,11 @@ import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { asPrettyString } from '../utils'; export class BoolFormat extends FieldFormat { - static id = 'boolean'; + static id = FIELD_FORMAT_IDS.BOOLEAN; static title = 'Boolean'; static fieldType = [KBN_FIELD_TYPES.BOOLEAN, KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; diff --git a/src/plugins/data/common/field_formats/converters/bytes.ts b/src/plugins/data/common/field_formats/converters/bytes.ts index 146f7afe74ccb..6c6df5eb7367d 100644 --- a/src/plugins/data/common/field_formats/converters/bytes.ts +++ b/src/plugins/data/common/field_formats/converters/bytes.ts @@ -18,9 +18,10 @@ */ import { NumeralFormat } from './numeral'; +import { FIELD_FORMAT_IDS } from '../types'; export class BytesFormat extends NumeralFormat { - static id = 'bytes'; + static id = FIELD_FORMAT_IDS.BYTES; static title = 'Bytes'; id = BytesFormat.id; diff --git a/src/plugins/data/common/field_formats/converters/color.test.ts b/src/plugins/data/common/field_formats/converters/color.test.ts index b7fcbf61227eb..f7aa26d449c7b 100644 --- a/src/plugins/data/common/field_formats/converters/color.test.ts +++ b/src/plugins/data/common/field_formats/converters/color.test.ts @@ -23,16 +23,19 @@ import { HTML_CONTEXT_TYPE } from '../content_types'; describe('Color Format', () => { describe('field is a number', () => { test('should add colors if the value is in range', () => { - const colorer = new ColorFormat({ - fieldType: 'number', - colors: [ - { - range: '100:150', - text: 'blue', - background: 'yellow', - }, - ], - }); + const colorer = new ColorFormat( + { + fieldType: 'number', + colors: [ + { + range: '100:150', + text: 'blue', + background: 'yellow', + }, + ], + }, + jest.fn() + ); expect(colorer.convert(99, HTML_CONTEXT_TYPE)).toBe('99'); expect(colorer.convert(100, HTML_CONTEXT_TYPE)).toBe( @@ -45,16 +48,19 @@ describe('Color Format', () => { }); test('should not convert invalid ranges', () => { - const colorer = new ColorFormat({ - fieldType: 'number', - colors: [ - { - range: '100150', - text: 'blue', - background: 'yellow', - }, - ], - }); + const colorer = new ColorFormat( + { + fieldType: 'number', + colors: [ + { + range: '100150', + text: 'blue', + background: 'yellow', + }, + ], + }, + jest.fn() + ); expect(colorer.convert(99, HTML_CONTEXT_TYPE)).toBe('99'); }); @@ -62,16 +68,19 @@ describe('Color Format', () => { describe('field is a string', () => { test('should add colors if the regex matches', () => { - const colorer = new ColorFormat({ - fieldType: 'string', - colors: [ - { - regex: 'A.*', - text: 'blue', - background: 'yellow', - }, - ], - }); + const colorer = new ColorFormat( + { + fieldType: 'string', + colors: [ + { + regex: 'A.*', + text: 'blue', + background: 'yellow', + }, + ], + }, + jest.fn() + ); const converter = colorer.getConverterFor(HTML_CONTEXT_TYPE) as Function; expect(converter('B', HTML_CONTEXT_TYPE)).toBe('B'); @@ -97,16 +106,19 @@ describe('Color Format', () => { }); test('returns original value (escaped) when regex is invalid', () => { - const colorer = new ColorFormat({ - fieldType: 'string', - colors: [ - { - regex: 'A.*', - text: 'blue', - background: 'yellow', - }, - ], - }); + const colorer = new ColorFormat( + { + fieldType: 'string', + colors: [ + { + regex: 'A.*', + text: 'blue', + background: 'yellow', + }, + ], + }, + jest.fn() + ); const converter = colorer.getConverterFor(HTML_CONTEXT_TYPE) as Function; expect(converter('<', HTML_CONTEXT_TYPE)).toBe('<'); diff --git a/src/plugins/data/common/field_formats/converters/color.ts b/src/plugins/data/common/field_formats/converters/color.ts index 6ba8bb97332e8..ffc72ba9a2c30 100644 --- a/src/plugins/data/common/field_formats/converters/color.ts +++ b/src/plugins/data/common/field_formats/converters/color.ts @@ -20,14 +20,14 @@ import { findLast, cloneDeep, template, escape } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { HtmlContextTypeConvert } from '../types'; +import { HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { asPrettyString } from '../utils'; import { DEFAULT_CONVERTER_COLOR } from '../constants'; const convertTemplate = template('<%- val %>'); export class ColorFormat extends FieldFormat { - static id = 'color'; + static id = FIELD_FORMAT_IDS.COLOR; static title = 'Color'; static fieldType = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index 8ab31e5784566..1c17e231cace8 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -17,14 +17,12 @@ * under the License. */ -import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { FieldFormat, IFieldFormatType } from '../field_format'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -const ID = 'custom'; - -export const createCustomFieldFormat = (convert: TextContextTypeConvert) => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => class CustomFieldFormat extends FieldFormat { - static id = ID; + static id = FIELD_FORMAT_IDS.CUSTOM; textConvert = convert; }; diff --git a/src/plugins/data/common/field_formats/converters/date.ts b/src/plugins/data/common/field_formats/converters/date.ts index 0017d5afb0608..06af64d9c17c2 100644 --- a/src/plugins/data/common/field_formats/converters/date.ts +++ b/src/plugins/data/common/field_formats/converters/date.ts @@ -19,28 +19,23 @@ import { memoize, noop } from 'lodash'; import moment from 'moment'; -import { FieldFormat, KBN_FIELD_TYPES, TextContextTypeConvert } from '../../index'; +import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; +import { FieldFormat } from '../field_format'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; export class DateFormat extends FieldFormat { - static id = 'date'; + static id = FIELD_FORMAT_IDS.DATE; static title = 'Date'; static fieldType = KBN_FIELD_TYPES.DATE; - private getConfig: Function; private memoizedConverter: Function = noop; private memoizedPattern: string = ''; private timeZone: string = ''; - constructor(params: Record, getConfig: Function) { - super(params); - - this.getConfig = getConfig; - } - getParamDefaults() { return { - pattern: this.getConfig('dateFormat'), - timezone: this.getConfig('dateFormat:tz'), + pattern: this.getConfig!('dateFormat'), + timezone: this.getConfig!('dateFormat:tz'), }; } diff --git a/src/plugins/data/common/field_formats/converters/date_nanos.ts b/src/plugins/data/common/field_formats/converters/date_nanos.ts index aef47f362bc97..8b0f8b111694e 100644 --- a/src/plugins/data/common/field_formats/converters/date_nanos.ts +++ b/src/plugins/data/common/field_formats/converters/date_nanos.ts @@ -21,7 +21,7 @@ import moment, { Moment } from 'moment'; import { memoize, noop } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; /** * Analyse the given moment.js format pattern for the fractional sec part (S,SS,SSS...) @@ -69,26 +69,19 @@ export function formatWithNanos( } export class DateNanosFormat extends FieldFormat { - static id = 'date_nanos'; + static id = FIELD_FORMAT_IDS.DATE_NANOS; static title = 'Date Nanos'; static fieldType = KBN_FIELD_TYPES.DATE; - private getConfig: Function; private memoizedConverter: Function = noop; private memoizedPattern: string = ''; private timeZone: string = ''; - constructor(params: Record, getConfig: Function) { - super(params); - - this.getConfig = getConfig; - } - getParamDefaults() { return { - pattern: this.getConfig('dateNanosFormat'), - fallbackPattern: this.getConfig('dateFormat'), - timezone: this.getConfig('dateFormat:tz'), + pattern: this.getConfig!('dateNanosFormat'), + fallbackPattern: this.getConfig!('dateFormat'), + timezone: this.getConfig!('dateFormat:tz'), }; } diff --git a/src/plugins/data/common/field_formats/converters/date_server.ts b/src/plugins/data/common/field_formats/converters/date_server.ts index 7ed2745a256c4..0c214e424f163 100644 --- a/src/plugins/data/common/field_formats/converters/date_server.ts +++ b/src/plugins/data/common/field_formats/converters/date_server.ts @@ -21,22 +21,20 @@ import { memoize, noop } from 'lodash'; import moment from 'moment-timezone'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; export class DateFormat extends FieldFormat { - static id = 'date'; + static id = FIELD_FORMAT_IDS.DATE; static title = 'Date'; static fieldType = KBN_FIELD_TYPES.DATE; - private getConfig: Function; private memoizedConverter: Function = noop; private memoizedPattern: string = ''; private timeZone: string = ''; constructor(params: Record, getConfig: Function) { - super(params); + super(params, getConfig); - this.getConfig = getConfig; this.memoizedConverter = memoize((val: any) => { if (val == null) { return '-'; @@ -66,8 +64,8 @@ export class DateFormat extends FieldFormat { getParamDefaults() { return { - pattern: this.getConfig('dateFormat'), - timezone: this.getConfig('dateFormat:tz'), + pattern: this.getConfig!('dateFormat'), + timezone: this.getConfig!('dateFormat:tz'), }; } diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index b892884475eec..d6205d54bd702 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -142,7 +142,10 @@ describe('Duration Format', () => { test(`should format ${input} ${inputFormat} through ${outputFormat}${ outputPrecision ? `, ${outputPrecision} decimals` : '' }`, () => { - const duration = new DurationFormat({ inputFormat, outputFormat, outputPrecision }); + const duration = new DurationFormat( + { inputFormat, outputFormat, outputPrecision }, + jest.fn() + ); expect(duration.convert(input)).toBe(output); }); }); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index 5eed523214ab3..d02de1a2fd889 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import moment, { unitOfTime, Duration } from 'moment'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; const ratioToSeconds: Record = { picoseconds: 0.000000000001, @@ -166,7 +166,7 @@ function parseInputAsDuration(val: number, inputFormat: string) { } export class DurationFormat extends FieldFormat { - static id = 'duration'; + static id = FIELD_FORMAT_IDS.DURATION; static title = 'Duration'; static fieldType = KBN_FIELD_TYPES.NUMBER; static inputFormats = inputFormats; diff --git a/src/plugins/data/common/field_formats/converters/ip.test.ts b/src/plugins/data/common/field_formats/converters/ip.test.ts index cc42d41adc4a1..a9a02d1a43ea8 100644 --- a/src/plugins/data/common/field_formats/converters/ip.test.ts +++ b/src/plugins/data/common/field_formats/converters/ip.test.ts @@ -23,7 +23,7 @@ describe('IP Address Format', () => { let ip: Record; beforeEach(() => { - ip = new IpFormat(); + ip = new IpFormat({}, jest.fn()); }); test('converts a value from a decimal to a string', () => { diff --git a/src/plugins/data/common/field_formats/converters/ip.ts b/src/plugins/data/common/field_formats/converters/ip.ts index 669f7d1b605d7..3e011e8d7dde8 100644 --- a/src/plugins/data/common/field_formats/converters/ip.ts +++ b/src/plugins/data/common/field_formats/converters/ip.ts @@ -19,10 +19,10 @@ import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; export class IpFormat extends FieldFormat { - static id = 'ip'; + static id = FIELD_FORMAT_IDS.IP; static title = 'IP Address'; static fieldType = KBN_FIELD_TYPES.IP; diff --git a/src/plugins/data/common/field_formats/converters/number.ts b/src/plugins/data/common/field_formats/converters/number.ts index e0c22f5350716..6969c1551e1cc 100644 --- a/src/plugins/data/common/field_formats/converters/number.ts +++ b/src/plugins/data/common/field_formats/converters/number.ts @@ -18,9 +18,10 @@ */ import { NumeralFormat } from './numeral'; +import { FIELD_FORMAT_IDS } from '../types'; export class NumberFormat extends NumeralFormat { - static id = 'number'; + static id = FIELD_FORMAT_IDS.NUMBER; static title = 'Number'; id = NumberFormat.id; diff --git a/src/plugins/data/common/field_formats/converters/numeral.ts b/src/plugins/data/common/field_formats/converters/numeral.ts index f7bf7ddfd1701..d8e46a480294f 100644 --- a/src/plugins/data/common/field_formats/converters/numeral.ts +++ b/src/plugins/data/common/field_formats/converters/numeral.ts @@ -37,15 +37,8 @@ export abstract class NumeralFormat extends FieldFormat { abstract id: string; abstract title: string; - protected getConfig: Function; - - constructor(params: Record, getConfig: Function) { - super(params); - this.getConfig = getConfig; - } - getParamDefaults = () => ({ - pattern: this.getConfig(`format:${this.id}:defaultPattern`), + pattern: this.getConfig!(`format:${this.id}:defaultPattern`), }); protected getConvertedValue(val: any): string { diff --git a/src/plugins/data/common/field_formats/converters/percent.ts b/src/plugins/data/common/field_formats/converters/percent.ts index f810f12377362..2ae32c7c77f07 100644 --- a/src/plugins/data/common/field_formats/converters/percent.ts +++ b/src/plugins/data/common/field_formats/converters/percent.ts @@ -18,17 +18,17 @@ */ import { NumeralFormat } from './numeral'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; export class PercentFormat extends NumeralFormat { - static id = 'percent'; + static id = FIELD_FORMAT_IDS.PERCENT; static title = 'Percentage'; id = PercentFormat.id; title = PercentFormat.title; getParamDefaults = () => ({ - pattern: this.getConfig('format:percent:defaultPattern'), + pattern: this.getConfig!('format:percent:defaultPattern'), fractional: true, }); diff --git a/src/plugins/data/common/field_formats/converters/relative_date.test.ts b/src/plugins/data/common/field_formats/converters/relative_date.test.ts index bde5aec0a5ab5..6311402a34b46 100644 --- a/src/plugins/data/common/field_formats/converters/relative_date.test.ts +++ b/src/plugins/data/common/field_formats/converters/relative_date.test.ts @@ -24,7 +24,7 @@ describe('Relative Date Format', () => { let convert: Function; beforeEach(() => { - const relativeDate = new RelativeDateFormat({}); + const relativeDate = new RelativeDateFormat({}, jest.fn()); convert = relativeDate.convert.bind(relativeDate); }); diff --git a/src/plugins/data/common/field_formats/converters/relative_date.ts b/src/plugins/data/common/field_formats/converters/relative_date.ts index caab8c3a2d7da..273b2cef28a03 100644 --- a/src/plugins/data/common/field_formats/converters/relative_date.ts +++ b/src/plugins/data/common/field_formats/converters/relative_date.ts @@ -20,17 +20,13 @@ import moment from 'moment'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; export class RelativeDateFormat extends FieldFormat { - static id = 'relative_date'; + static id = FIELD_FORMAT_IDS.RELATIVE_DATE; static title = 'Relative Date'; static fieldType = KBN_FIELD_TYPES.DATE; - constructor(params: Record) { - super(params); - } - textConvert: TextContextTypeConvert = val => { if (val === null || val === undefined) { return '-'; diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index 35eb14ca59ebb..54977c7e66976 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -24,7 +24,7 @@ import { noWhiteSpace } from '../../../../../legacy/core_plugins/kibana/common/u import { shortenDottedString } from '../../../../../legacy/core_plugins/kibana/common/utils/shorten_dotted_string'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, HtmlContextTypeConvert } from '../types'; +import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; const templateHtml = `
@@ -37,18 +37,10 @@ const templateHtml = ` const doTemplate = template(noWhiteSpace(templateHtml)); export class SourceFormat extends FieldFormat { - static id = '_source'; + static id = FIELD_FORMAT_IDS._SOURCE; static title = '_source'; static fieldType = KBN_FIELD_TYPES._SOURCE; - private getConfig: Function; - - constructor(params: Record, getConfig: Function) { - super(params); - - this.getConfig = getConfig; - } - textConvert: TextContextTypeConvert = value => JSON.stringify(value); htmlConvert: HtmlContextTypeConvert = (value, field, hit) => { @@ -62,7 +54,7 @@ export class SourceFormat extends FieldFormat { const formatted = field.indexPattern.formatHit(hit); const highlightPairs: any[] = []; const sourcePairs: any[] = []; - const isShortDots = this.getConfig('shortDots:enable'); + const isShortDots = this.getConfig!('shortDots:enable'); keys(formatted).forEach(key => { const pairs = highlights[key] ? highlightPairs : sourcePairs; diff --git a/src/plugins/data/common/field_formats/converters/static_lookup.ts b/src/plugins/data/common/field_formats/converters/static_lookup.ts index 29c64fd4f4046..419e7c786640b 100644 --- a/src/plugins/data/common/field_formats/converters/static_lookup.ts +++ b/src/plugins/data/common/field_formats/converters/static_lookup.ts @@ -19,7 +19,7 @@ import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; function convertLookupEntriesToMap(lookupEntries: any[]) { return lookupEntries.reduce( @@ -32,7 +32,7 @@ function convertLookupEntriesToMap(lookupEntries: any[]) { } export class StaticLookupFormat extends FieldFormat { - static id = 'static_lookup'; + static id = FIELD_FORMAT_IDS.STATIC_LOOKUP; static title = 'Static Lookup'; static fieldType = [ KBN_FIELD_TYPES.STRING, diff --git a/src/plugins/data/common/field_formats/converters/string.test.ts b/src/plugins/data/common/field_formats/converters/string.test.ts index bb0fd5cc011b6..b2d96ed5c0308 100644 --- a/src/plugins/data/common/field_formats/converters/string.test.ts +++ b/src/plugins/data/common/field_formats/converters/string.test.ts @@ -21,30 +21,42 @@ import { StringFormat } from './string'; describe('String Format', () => { test('convert a string to lower case', () => { - const string = new StringFormat({ - transform: 'lower', - }); + const string = new StringFormat( + { + transform: 'lower', + }, + jest.fn() + ); expect(string.convert('Kibana')).toBe('kibana'); }); test('convert a string to upper case', () => { - const string = new StringFormat({ - transform: 'upper', - }); + const string = new StringFormat( + { + transform: 'upper', + }, + jest.fn() + ); expect(string.convert('Kibana')).toBe('KIBANA'); }); test('decode a base64 string', () => { - const string = new StringFormat({ - transform: 'base64', - }); + const string = new StringFormat( + { + transform: 'base64', + }, + jest.fn() + ); expect(string.convert('Zm9vYmFy')).toBe('foobar'); }); test('convert a string to title case', () => { - const string = new StringFormat({ - transform: 'title', - }); + const string = new StringFormat( + { + transform: 'title', + }, + jest.fn() + ); expect(string.convert('PLEASE DO NOT SHOUT')).toBe('Please Do Not Shout'); expect(string.convert('Mean, variance and standard_deviation.')).toBe( 'Mean, Variance And Standard_deviation.' @@ -53,24 +65,33 @@ describe('String Format', () => { }); test('convert a string to short case', () => { - const string = new StringFormat({ - transform: 'short', - }); + const string = new StringFormat( + { + transform: 'short', + }, + jest.fn() + ); expect(string.convert('dot.notated.string')).toBe('d.n.string'); }); test('convert a string to unknown transform case', () => { - const string = new StringFormat({ - transform: 'unknown_transform', - }); + const string = new StringFormat( + { + transform: 'unknown_transform', + }, + jest.fn() + ); const value = 'test test test'; expect(string.convert(value)).toBe(value); }); test('decode a URL Param string', () => { - const string = new StringFormat({ - transform: 'urlparam', - }); + const string = new StringFormat( + { + transform: 'urlparam', + }, + jest.fn() + ); expect(string.convert('%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98')).toBe('안녕 키바나'); }); }); diff --git a/src/plugins/data/common/field_formats/converters/string.ts b/src/plugins/data/common/field_formats/converters/string.ts index 82547c0b0dee5..0edd219ca60f9 100644 --- a/src/plugins/data/common/field_formats/converters/string.ts +++ b/src/plugins/data/common/field_formats/converters/string.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { asPrettyString } from '../index'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; // @ts-ignore import { shortenDottedString } from '../../../../../legacy/core_plugins/kibana/common/utils/shorten_dotted_string'; @@ -72,7 +72,7 @@ const TRANSFORM_OPTIONS = [ const DEFAULT_TRANSFORM_OPTION = false; export class StringFormat extends FieldFormat { - static id = 'string'; + static id = FIELD_FORMAT_IDS.STRING; static title = 'String'; static fieldType = [ KBN_FIELD_TYPES.NUMBER, diff --git a/src/plugins/data/common/field_formats/converters/truncate.test.ts b/src/plugins/data/common/field_formats/converters/truncate.test.ts index 7de4bdb3dedfe..472d9673346d7 100644 --- a/src/plugins/data/common/field_formats/converters/truncate.test.ts +++ b/src/plugins/data/common/field_formats/converters/truncate.test.ts @@ -21,25 +21,25 @@ import { TruncateFormat } from './truncate'; describe('String TruncateFormat', () => { test('truncate large string', () => { - const truncate = new TruncateFormat({ fieldLength: 4 }); + const truncate = new TruncateFormat({ fieldLength: 4 }, jest.fn()); expect(truncate.convert('This is some text')).toBe('This...'); }); test('does not truncate large string when field length is not a string', () => { - const truncate = new TruncateFormat({ fieldLength: 'not number' }); + const truncate = new TruncateFormat({ fieldLength: 'not number' }, jest.fn()); expect(truncate.convert('This is some text')).toBe('This is some text'); }); test('does not truncate large string when field length is null', () => { - const truncate = new TruncateFormat({ fieldLength: null }); + const truncate = new TruncateFormat({ fieldLength: null }, jest.fn()); expect(truncate.convert('This is some text')).toBe('This is some text'); }); test('does not truncate large string when field length larger than the text', () => { - const truncate = new TruncateFormat({ fieldLength: 100000 }); + const truncate = new TruncateFormat({ fieldLength: 100000 }, jest.fn()); expect(truncate.convert('This is some text')).toBe('This is some text'); }); diff --git a/src/plugins/data/common/field_formats/converters/truncate.ts b/src/plugins/data/common/field_formats/converters/truncate.ts index acccf2a20c69a..dc25d71ec95d7 100644 --- a/src/plugins/data/common/field_formats/converters/truncate.ts +++ b/src/plugins/data/common/field_formats/converters/truncate.ts @@ -20,12 +20,12 @@ import { trunc } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; const omission = '...'; export class TruncateFormat extends FieldFormat { - static id = 'truncate'; + static id = FIELD_FORMAT_IDS.TRUNCATE; static title = 'Truncated String'; static fieldType = KBN_FIELD_TYPES.STRING; diff --git a/src/plugins/data/common/field_formats/converters/url.ts b/src/plugins/data/common/field_formats/converters/url.ts index 6c00f11a408dc..bd68dedf38a67 100644 --- a/src/plugins/data/common/field_formats/converters/url.ts +++ b/src/plugins/data/common/field_formats/converters/url.ts @@ -22,7 +22,7 @@ import { escape, memoize } from 'lodash'; import { getHighlightHtml } from '../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, HtmlContextTypeConvert } from '../types'; +import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; const templateMatchRE = /{{([\s\S]+?)}}/g; const whitelistUrlSchemes = ['http://', 'https://']; @@ -50,7 +50,7 @@ const URL_TYPES = [ const DEFAULT_URL_TYPE = 'a'; export class UrlFormat extends FieldFormat { - static id = 'url'; + static id = FIELD_FORMAT_IDS.URL; static title = 'Url'; static fieldType = [ KBN_FIELD_TYPES.NUMBER, diff --git a/src/plugins/data/common/field_formats/field_format.test.ts b/src/plugins/data/common/field_formats/field_format.test.ts index d4789f12bdee9..2229601994496 100644 --- a/src/plugins/data/common/field_formats/field_format.test.ts +++ b/src/plugins/data/common/field_formats/field_format.test.ts @@ -32,7 +32,7 @@ const getTestFormat = ( textConvert = textConvert; htmlConvert = htmlConvert; - })(_params); + })(_params, jest.fn()); describe('FieldFormat class', () => { describe('params', () => { diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index e99894ca56167..dd445a33f21c5 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -19,7 +19,12 @@ import { transform, size, cloneDeep, get, defaults } from 'lodash'; import { createCustomFieldFormat } from './converters/custom'; -import { ContentType, FieldFormatConvert, FieldFormatConvertFunction } from './types'; +import { + ContentType, + FIELD_FORMAT_IDS, + FieldFormatConvert, + FieldFormatConvertFunction, +} from './types'; import { htmlContentTypeSetup, textContentTypeSetup, @@ -68,7 +73,16 @@ export abstract class FieldFormat { */ public type: any = this.constructor; - constructor(public _params: any = {}) {} + protected readonly _params: any; + protected getConfig: Function | undefined; + + constructor(_params: any = {}, getConfig?: Function) { + this._params = _params; + + if (getConfig) { + this.getConfig = getConfig; + } + } /** * Convert a raw value to a formatted string @@ -170,7 +184,7 @@ export abstract class FieldFormat { }; } - static from(convertFn: FieldFormatConvertFunction): ReturnType { + static from(convertFn: FieldFormatConvertFunction): IFieldFormatType { return createCustomFieldFormat(convertFn); } @@ -183,3 +197,11 @@ export abstract class FieldFormat { } export type IFieldFormat = PublicMethodsOf; +/** + * @string id type is needed for creating custom converters. + */ +export type IFieldFormatId = FIELD_FORMAT_IDS | string; +export type IFieldFormatType = (new (params?: any, getConfig?: Function) => FieldFormat) & { + id: IFieldFormatId; + fieldType: string | string[]; +}; diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 5d04a69e4dc50..b751b097b5ed2 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -18,7 +18,7 @@ */ export { HTML_CONTEXT_TYPE, TEXT_CONTEXT_TYPE } from './content_types'; -export { FieldFormat } from './field_format'; +export { FieldFormat, IFieldFormatType, IFieldFormatId } from './field_format'; export { getHighlightRequest, asPrettyString, getHighlightHtml } from './utils'; export * from './converters'; export * from './constants'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 626bab297392b..fc8e6e20a1a96 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -42,3 +42,23 @@ export interface FieldFormatConvert { text: TextContextTypeConvert; html: HtmlContextTypeConvert; } + +/** @public **/ +export enum FIELD_FORMAT_IDS { + _SOURCE = '_source', + BOOLEAN = 'boolean', + BYTES = 'bytes', + COLOR = 'color', + CUSTOM = 'custom', + DATE = 'date', + DATE_NANOS = 'date_nanos', + DURATION = 'duration', + IP = 'ip', + NUMBER = 'number', + PERCENT = 'percent', + RELATIVE_DATE = 'relative_date', + STATIC_LOOKUP = 'static_lookup', + STRING = 'string', + TRUNCATE = 'truncate', + URL = 'url', +} diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts index 5312f1be6c26c..8788d4b690aba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts @@ -20,36 +20,19 @@ import { getHighlightRequest } from './highlight_request'; describe('getHighlightRequest', () => { - let configMock: Record; - const getConfig = (key: string) => configMock[key]; const queryStringQuery = { query_string: { query: 'foo' } }; - beforeEach(function() { - configMock = {}; - configMock['doc_table:highlight'] = true; - }); - test('should be a function', () => { expect(getHighlightRequest).toBeInstanceOf(Function); }); test('should not modify the original query', () => { - getHighlightRequest(queryStringQuery, getConfig); + getHighlightRequest(queryStringQuery, true); expect(queryStringQuery.query_string).not.toHaveProperty('highlight'); }); test('should return undefined if highlighting is turned off', () => { - configMock['doc_table:highlight'] = false; - const request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).toBe(undefined); - }); - - test('should enable/disable highlighting if config is changed', () => { - let request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).not.toBe(undefined); - - configMock['doc_table:highlight'] = false; - request = getHighlightRequest(queryStringQuery, getConfig); + const request = getHighlightRequest(queryStringQuery, false); expect(request).toBe(undefined); }); }); diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts index 199a73e692e39..8012ab59c33ba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts @@ -21,8 +21,8 @@ import { highlightTags } from './highlight_tags'; const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) -export function getHighlightRequest(query: any, getConfig: Function) { - if (!getConfig('doc_table:highlight')) return; +export function getHighlightRequest(query: any, shouldHighlight: boolean) { + if (!shouldHighlight) return; return { pre_tags: [highlightTags.pre], diff --git a/src/plugins/data/common/index_patterns/fields/index.ts b/src/plugins/data/common/index_patterns/fields/index.ts index d8f7b5091eb8f..2b43dffa8c161 100644 --- a/src/plugins/data/common/index_patterns/fields/index.ts +++ b/src/plugins/data/common/index_patterns/fields/index.ts @@ -18,3 +18,4 @@ */ export * from './types'; +export { isFilterable } from './utils'; diff --git a/src/plugins/data/common/index_patterns/fields/utils.ts b/src/plugins/data/common/index_patterns/fields/utils.ts new file mode 100644 index 0000000000000..c7bec5e5ad347 --- /dev/null +++ b/src/plugins/data/common/index_patterns/fields/utils.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getFilterableKbnTypeNames, IFieldType } from '../..'; + +const filterableTypes = getFilterableKbnTypeNames(); + +export function isFilterable(field: IFieldType): boolean { + return ( + field.name === '_id' || + field.scripted || + Boolean(field.searchable && filterableTypes.includes(field.type)) + ); +} diff --git a/src/plugins/data/common/index_patterns/utils.test.ts b/src/plugins/data/common/index_patterns/utils.test.ts new file mode 100644 index 0000000000000..e2707d469a317 --- /dev/null +++ b/src/plugins/data/common/index_patterns/utils.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { isFilterable } from '.'; +import { IFieldType } from './fields'; + +const mockField = { + name: 'foo', + scripted: false, + searchable: true, + type: 'string', +} as IFieldType; + +describe('isFilterable', () => { + describe('types', () => { + it('should return true for filterable types', () => { + ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { + expect(isFilterable({ ...mockField, type })).toBe(true); + }); + }); + + it('should return false for filterable types if the field is not searchable', () => { + ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { + expect(isFilterable({ ...mockField, type, searchable: false })).toBe(false); + }); + }); + + it('should return false for un-filterable types', () => { + ['geo_point', 'geo_shape', 'attachment', 'murmur3', '_source', 'unknown', 'conflict'].forEach( + type => { + expect(isFilterable({ ...mockField, type })).toBe(false); + } + ); + }); + }); + + it('should return true for scripted fields', () => { + expect(isFilterable({ ...mockField, scripted: true, searchable: false })).toBe(true); + }); + + it('should return true for the _id field', () => { + expect(isFilterable({ ...mockField, name: '_id' })).toBe(true); + }); +}); diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts index 21d58bcd0f78c..11c62e8f86dce 100644 --- a/src/plugins/data/common/kbn_field_types/types.ts +++ b/src/plugins/data/common/kbn_field_types/types.ts @@ -27,9 +27,10 @@ export interface KbnFieldTypeOptions { /** @public **/ export enum ES_FIELD_TYPES { - _TYPE = '_type', _ID = '_id', + _INDEX = '_index', _SOURCE = '_source', + _TYPE = '_type', STRING = 'string', TEXT = 'text', diff --git a/src/plugins/data/public/field_formats_provider/field_formats.ts b/src/plugins/data/public/field_formats_provider/field_formats.ts new file mode 100644 index 0000000000000..f46994c209ded --- /dev/null +++ b/src/plugins/data/public/field_formats_provider/field_formats.ts @@ -0,0 +1,226 @@ +/* + * 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 { forOwn, isFunction, memoize } from 'lodash'; +import { UiSettingsClientContract } from 'kibana/public'; +import { + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + FIELD_FORMAT_IDS, + IFieldFormatType, + IFieldFormatId, + FieldFormat, +} from '../../common'; +import { FieldType } from './types'; + +export class FieldFormatRegisty { + private fieldFormats: Map; + private uiSettings!: UiSettingsClientContract; + private defaultMap: Record; + + constructor() { + this.fieldFormats = new Map(); + this.defaultMap = {}; + } + + getConfig = (key: string, override?: any) => this.uiSettings.get(key, override); + + init(uiSettings: UiSettingsClientContract) { + this.uiSettings = uiSettings; + + this.parseDefaultTypeMap(this.uiSettings.get('format:defaultTypeMap')); + + this.uiSettings.getUpdate$().subscribe(({ key, newValue }) => { + if (key === 'format:defaultTypeMap') { + this.parseDefaultTypeMap(newValue); + } + }); + } + + /** + * Get the id of the default type for this field type + * using the format:defaultTypeMap config map + * + * @param {KBN_FIELD_TYPES} fieldType - the field type + * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types + * @return {FieldType} + */ + getDefaultConfig = (fieldType: KBN_FIELD_TYPES, esTypes?: ES_FIELD_TYPES[]): FieldType => { + const type = this.getDefaultTypeName(fieldType, esTypes); + + return ( + (this.defaultMap && this.defaultMap[type]) || { id: FIELD_FORMAT_IDS.STRING, params: {} } + ); + }; + + /** + * Get a derived FieldFormat class by its id. + * + * @param {IFieldFormatId} formatId - the format id + * @return {FieldFormat} + */ + getType = (formatId: IFieldFormatId): IFieldFormatType | undefined => { + return this.fieldFormats.get(formatId); + }; + + /** + * Get the default FieldFormat type (class) for + * a field type, using the format:defaultTypeMap. + * used by the field editor + * + * @param {KBN_FIELD_TYPES} fieldType + * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types + * @return {FieldFormat} + */ + getDefaultType = ( + fieldType: KBN_FIELD_TYPES, + esTypes: ES_FIELD_TYPES[] + ): IFieldFormatType | undefined => { + const config = this.getDefaultConfig(fieldType, esTypes); + + return this.getType(config.id); + }; + + /** + * Get the name of the default type for ES types like date_nanos + * using the format:defaultTypeMap config map + * + * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types + * @return {ES_FIELD_TYPES} + */ + getTypeNameByEsTypes = (esTypes: ES_FIELD_TYPES[] | undefined): ES_FIELD_TYPES | undefined => { + if (!Array.isArray(esTypes)) { + return; + } + + return esTypes.find(type => this.defaultMap[type] && this.defaultMap[type].es); + }; + + /** + * Get the default FieldFormat type name for + * a field type, using the format:defaultTypeMap. + * + * @param {KBN_FIELD_TYPES} fieldType + * @param {ES_FIELD_TYPES[]} esTypes + * @return {ES_FIELD_TYPES | String} + */ + getDefaultTypeName = ( + fieldType: KBN_FIELD_TYPES, + esTypes?: ES_FIELD_TYPES[] + ): ES_FIELD_TYPES | KBN_FIELD_TYPES => { + const esType = this.getTypeNameByEsTypes(esTypes); + + return esType || fieldType; + }; + + /** + * Get the singleton instance of the FieldFormat type by its id. + * + * @param {IFieldFormatId} formatId + * @return {FIELD_FORMATS_INSTANCES[number]} + */ + getInstance = memoize( + (formatId: IFieldFormatId): FieldFormat => { + const DerivedFieldFormat = this.getType(formatId); + + if (!DerivedFieldFormat) { + throw new Error(`Field Format '${formatId}' not found!`); + } + + return new DerivedFieldFormat({}, this.getConfig); + } + ); + + /** + * Get the default fieldFormat instance for a field format. + * + * @param {KBN_FIELD_TYPES} fieldType + * @param {ES_FIELD_TYPES[]} esTypes + * @return {FieldFormat} + */ + getDefaultInstancePlain(fieldType: KBN_FIELD_TYPES, esTypes?: ES_FIELD_TYPES[]): FieldFormat { + const conf = this.getDefaultConfig(fieldType, esTypes); + + const DerivedFieldFormat = this.getType(conf.id); + + if (!DerivedFieldFormat) { + throw new Error(`Field Format '${conf.id}' not found!`); + } + + return new DerivedFieldFormat(conf.params, this.getConfig); + } + /** + * Returns a cache key built by the given variables for caching in memoized + * Where esType contains fieldType, fieldType is returned + * -> kibana types have a higher priority in that case + * -> would lead to failing tests that match e.g. date format with/without esTypes + * https://lodash.com/docs#memoize + * + * @param {KBN_FIELD_TYPES} fieldType + * @param {ES_FIELD_TYPES[]} esTypes + * @return {String} + */ + getDefaultInstanceCacheResolver(fieldType: KBN_FIELD_TYPES, esTypes: ES_FIELD_TYPES[]): string { + // @ts-ignore + return Array.isArray(esTypes) && esTypes.indexOf(fieldType) === -1 + ? [fieldType, ...esTypes].join('-') + : fieldType; + } + + /** + * Get filtered list of field formats by format type + * + * @param {KBN_FIELD_TYPES} fieldType + * @return {FieldFormat[]} + */ + getByFieldType(fieldType: KBN_FIELD_TYPES): IFieldFormatType[] { + return [...this.fieldFormats.values()].filter( + (format: IFieldFormatType) => format.fieldType.indexOf(fieldType) !== -1 + ); + } + + /** + * Get the default fieldFormat instance for a field format. + * It's a memoized function that builds and reads a cache + * + * @param {KBN_FIELD_TYPES} fieldType + * @param {ES_FIELD_TYPES[]} esTypes + * @return {FieldFormat} + */ + getDefaultInstance = memoize(this.getDefaultInstancePlain, this.getDefaultInstanceCacheResolver); + + parseDefaultTypeMap(value: any) { + this.defaultMap = value; + forOwn(this, fn => { + if (isFunction(fn) && fn.cache) { + // clear all memoize caches + // @ts-ignore + fn.cache = new memoize.Cache(); + } + }); + } + + register = (fieldFormats: IFieldFormatType[]) => { + fieldFormats.forEach(fieldFormat => { + this.fieldFormats.set(fieldFormat.id, fieldFormat); + }); + + return this; + }; +} diff --git a/src/plugins/data/public/field_formats_provider/field_formats_service.ts b/src/plugins/data/public/field_formats_provider/field_formats_service.ts new file mode 100644 index 0000000000000..b144ea7ec2530 --- /dev/null +++ b/src/plugins/data/public/field_formats_provider/field_formats_service.ts @@ -0,0 +1,87 @@ +/* + * 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 { UiSettingsClientContract } from 'src/core/public'; +import { FieldFormatRegisty } from './field_formats'; + +import { + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DurationFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, +} from '../../common/'; + +/** + * Field Format Service + * @internal + */ +interface FieldFormatsServiceDependencies { + uiSettings: UiSettingsClientContract; +} + +export class FieldFormatsService { + private readonly fieldFormats: FieldFormatRegisty = new FieldFormatRegisty(); + + public setup({ uiSettings }: FieldFormatsServiceDependencies) { + this.fieldFormats.init(uiSettings); + + this.fieldFormats.register([ + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DurationFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, + ]); + + return this.fieldFormats as FieldFormatsSetup; + } + + public start() { + return this.fieldFormats as FieldFormatsStart; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type FieldFormatsSetup = Omit; +export type FieldFormatsStart = Omit; diff --git a/src/plugins/data/public/field_formats_provider/index.ts b/src/plugins/data/public/field_formats_provider/index.ts new file mode 100644 index 0000000000000..442d877c5316a --- /dev/null +++ b/src/plugins/data/public/field_formats_provider/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FieldFormatRegisty } from './field_formats'; // TODO: Try to remove +export { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats_service'; diff --git a/src/plugins/data/public/field_formats_provider/types.ts b/src/plugins/data/public/field_formats_provider/types.ts new file mode 100644 index 0000000000000..fc33bf4d38f85 --- /dev/null +++ b/src/plugins/data/public/field_formats_provider/types.ts @@ -0,0 +1,26 @@ +/* + * 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 { IFieldFormatId } from '../../common'; + +export interface FieldType { + id: IFieldFormatId; + params: Record; + es?: boolean; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4477c6defbc81..ace0b44378b45 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -29,8 +29,12 @@ export { DataPublicPlugin as Plugin }; export * from '../common'; export * from './autocomplete_provider'; +export * from './field_formats_provider'; + export * from './types'; export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; + +export * from './ui'; diff --git a/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts new file mode 100644 index 0000000000000..777a12c7e2884 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts @@ -0,0 +1,33 @@ +/* + * 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 { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; + +export async function getIndexPatternTitle( + client: SavedObjectsClientContract, + indexPatternId: string +): Promise> { + const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; + + if (savedObject.error) { + throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); + } + + return savedObject.attributes.title; +} diff --git a/src/plugins/data/public/index_patterns/lib/index.ts b/src/plugins/data/public/index_patterns/lib/index.ts new file mode 100644 index 0000000000000..d1c229513aa33 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getIndexPatternTitle } from './get_index_pattern_title'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 4aae63c24d7fc..ceb57b4a3a564 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Plugin } from '.'; +import { FieldFormatRegisty, Plugin, FieldFormatsStart, FieldFormatsSetup } from '.'; import { searchSetupMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; @@ -29,11 +29,29 @@ const autocompleteMock: any = { clearProviders: jest.fn(), }; +const fieldFormatsMock: PublicMethodsOf = { + getByFieldType: jest.fn(), + getConfig: jest.fn(), + getDefaultConfig: jest.fn(), + getDefaultInstance: jest.fn() as any, + getDefaultInstanceCacheResolver: jest.fn(), + getDefaultInstancePlain: jest.fn(), + getDefaultType: jest.fn(), + getDefaultTypeName: jest.fn(), + getInstance: jest.fn() as any, + getType: jest.fn(), + getTypeNameByEsTypes: jest.fn(), + init: jest.fn(), + register: jest.fn(), + parseDefaultTypeMap: jest.fn(), +}; + const createSetupContract = (): Setup => { const querySetupMock = queryServiceMock.createSetupContract(); const setupContract = { autocomplete: autocompleteMock, search: searchSetupMock, + fieldFormats: fieldFormatsMock as FieldFormatsSetup, query: querySetupMock, }; @@ -46,7 +64,11 @@ const createStartContract = (): Start => { autocomplete: autocompleteMock, getSuggestions: jest.fn(), search: { search: jest.fn() }, + fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, + ui: { + IndexPatternSelect: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 79db34c022b39..d8c45b6786c0c 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -17,29 +17,35 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { Storage } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from './types'; import { AutocompleteProviderRegister } from './autocomplete_provider'; import { getSuggestionsProvider } from './suggestions_provider'; import { SearchService } from './search/search_service'; +import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; +import { createIndexPatternSelect } from './ui/index_pattern_select'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); private readonly searchService: SearchService; + private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); + this.fieldFormatsService = new FieldFormatsService(); } public setup(core: CoreSetup): DataPublicPluginSetup { const storage = new Storage(window.localStorage); + return { autocomplete: this.autocomplete, search: this.searchService.setup(core), + fieldFormats: this.fieldFormatsService.setup(core), query: this.queryService.setup({ uiSettings: core.uiSettings, storage, @@ -52,7 +58,11 @@ export class DataPublicPlugin implements Plugin { + test('should return the key for matching multi polygon filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + type: esFilters.FILTERS.SPATIAL_FILTER, + } as esFilters.FilterMeta, + query: { + bool: { + should: [ + { + geo_polygon: { + geoCoordinates: { points: [] }, + }, + }, + ], + }, + }, + } as esFilters.Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'query'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', esFilters.FILTERS.SPATIAL_FILTER); + }); + + test('should return the key for matching polygon filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + type: esFilters.FILTERS.SPATIAL_FILTER, + } as esFilters.FilterMeta, + geo_polygon: { + geoCoordinates: { points: [] }, + }, + } as esFilters.Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'geo_polygon'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', esFilters.FILTERS.SPATIAL_FILTER); + }); + + test('should return undefined for none matching', async done => { + const filter = { + meta: { + alias: 'my spatial filter', + } as esFilters.FilterMeta, + geo_polygon: { + geoCoordinates: { points: [] }, + }, + } as esFilters.Filter; + + try { + mapSpatialFilter(filter); + } catch (e) { + expect(e).toBe(filter); + + done(); + } + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts new file mode 100644 index 0000000000000..3cf1cf7835e69 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -0,0 +1,40 @@ +/* + * 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 { esFilters } from '../../../../../common'; + +// Use mapSpatialFilter mapper to avoid bloated meta with value and params for spatial filters. +export const mapSpatialFilter = (filter: esFilters.Filter) => { + const metaProperty = /(^\$|meta)/; + const key = Object.keys(filter).find(item => { + return !item.match(metaProperty); + }); + if ( + key && + filter.meta && + filter.meta.alias && + filter.meta.type === esFilters.FILTERS.SPATIAL_FILTER + ) { + return { + key, + type: filter.meta.type, + value: '', + }; + } + throw filter; +}; diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a1eb36c2ee028..a8eb3a3fe8102 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import sinon from 'sinon'; -import { Filter, getTime } from './get_time'; +import { getTime } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -43,8 +43,8 @@ describe('get_time', () => { ], } as any, { from: 'now-60y', to: 'now' } - ) as Filter; - expect(filter.range.date).toEqual({ + ); + expect(filter!.range.date).toEqual({ gte: '1940-02-01T00:00:00.000Z', lte: '2000-02-01T00:00:00.000Z', format: 'strict_date_optional_time', diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index 41ad1a49af0ff..d3fbc17734f81 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -21,22 +21,13 @@ import dateMath from '@elastic/datemath'; import { TimeRange } from '../../../common'; // TODO: remove this -import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public/index_patterns'; +import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public'; +import { esFilters } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; } -interface RangeFilter { - gte?: string | number; - lte?: string | number; - format: string; -} - -export interface Filter { - range: { [s: string]: RangeFilter }; -} - export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOptions = {}) { return { min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }), @@ -45,10 +36,10 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp } export function getTime( - indexPattern: IndexPattern, + indexPattern: IndexPattern | undefined, timeRange: TimeRange, forceNow?: Date -): Filter | undefined { +) { if (!indexPattern) { // in CI, we sometimes seem to fail here. return; @@ -66,17 +57,13 @@ export function getTime( if (!bounds) { return; } - const filter: Filter = { - range: { [timefield.name]: { format: 'strict_date_optional_time' } }, - }; - - if (bounds.min) { - filter.range[timefield.name].gte = bounds.min.toISOString(); - } - - if (bounds.max) { - filter.range[timefield.name].lte = bounds.max.toISOString(); - } - - return filter; + return esFilters.buildRangeFilter( + timefield, + { + ...(bounds.min && { gte: bounds.min.toISOString() }), + ...(bounds.max && { lte: bounds.max.toISOString() }), + format: 'strict_date_optional_time', + }, + indexPattern + ); } diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index 4dabbb557e9db..fe73fd85b164d 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -37,7 +37,7 @@ export class TimeHistory { } add(time: TimeRange) { - if (!time) { + if (!time || !time.from || !time.to) { return; } diff --git a/src/plugins/data/public/search/i_search.ts b/src/plugins/data/public/search/i_search.ts index 0e256b960ffa3..a39ef3e3e7571 100644 --- a/src/plugins/data/public/search/i_search.ts +++ b/src/plugins/data/public/search/i_search.ts @@ -49,11 +49,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], - options: ISearchOptions, + options?: ISearchOptions, strategy?: T ) => Observable; export type ISearch = ( request: IRequestTypesMap[T], - options: ISearchOptions + options?: ISearchOptions ) => Observable; diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c412bbb3b104a..3885a97a98571 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -34,7 +34,7 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { const search: ISearch = ( request: ISyncSearchRequest, - options: ISearchOptions + options: ISearchOptions = {} ) => { const response: Promise = context.core.http.fetch( `/internal/search/${request.serverStrategy}`, diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index 40a5e7d18f8d9..01e68288bd655 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -19,3 +19,4 @@ export { stubIndexPattern } from './index_patterns/index_pattern.stub'; export { stubFields } from './index_patterns/field.stub'; +export * from '../common/es_query/filters/stubs'; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 9939815c1efd1..c0c96372f9f59 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -17,15 +17,19 @@ * under the License. */ -export * from './autocomplete_provider/types'; - +import { CoreStart } from 'src/core/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.'; +import { FieldFormatsSetup, FieldFormatsStart } from './field_formats_provider'; import { ISearchSetup, ISearchStart } from './search'; import { IGetSuggestions } from './suggestions_provider/types'; import { QuerySetup, QueryStart } from './query'; +import { IndexPatternSelectProps } from './ui/index_pattern_select'; + export interface DataPublicPluginSetup { autocomplete: AutocompletePublicPluginSetup; search: ISearchSetup; + fieldFormats: FieldFormatsSetup; query: QuerySetup; } @@ -33,7 +37,22 @@ export interface DataPublicPluginStart { autocomplete: AutocompletePublicPluginStart; getSuggestions: IGetSuggestions; search: ISearchStart; + fieldFormats: FieldFormatsStart; query: QueryStart; + ui: { + IndexPatternSelect: React.ComponentType; + }; } +export * from './autocomplete_provider/types'; export { IGetSuggestions } from './suggestions_provider/types'; + +export interface IDataPluginServices extends Partial { + appName: string; + uiSettings: CoreStart['uiSettings']; + savedObjects: CoreStart['savedObjects']; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + storage: IStorageWrapper; + data: DataPublicPluginStart; +} diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx similarity index 89% rename from src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index ab52d56841612..affbb8acecb20 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,14 +30,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { IndexPattern } from '../../index_patterns'; -import { FilterLabel } from '../filter_bar/filter_editor/lib/filter_label'; -import { mapAndFlattenFilters, esFilters } from '../../../../../../plugins/data/public'; -import { getDisplayValueFromFilter } from '../filter_bar/filter_editor/lib/get_display_value'; +import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { FilterLabel } from '../filter_bar'; interface Props { filters: esFilters.Filter[]; - indexPatterns: IndexPattern[]; + indexPatterns: IIndexPattern[]; onCancel: () => void; onSubmit: (filters: esFilters.Filter[]) => void; } @@ -58,7 +56,7 @@ export class ApplyFiltersPopoverContent extends Component { }; } private getLabel(filter: esFilters.Filter) { - const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); return ; } diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx new file mode 100644 index 0000000000000..71a042adffa39 --- /dev/null +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -0,0 +1,41 @@ +/* + * 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 { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; +import { IIndexPattern, esFilters } from '../..'; + +type CancelFnType = () => void; +type SubmitFnType = (filters: esFilters.Filter[]) => void; + +export const applyFiltersPopover = ( + filters: esFilters.Filter[], + indexPatterns: IIndexPattern[], + onCancel: CancelFnType, + onSubmit: SubmitFnType +) => { + return ( + + ); +}; diff --git a/src/plugins/data/public/ui/apply_filters/index.ts b/src/plugins/data/public/ui/apply_filters/index.ts new file mode 100644 index 0000000000000..93c1245e1ffb0 --- /dev/null +++ b/src/plugins/data/public/ui/apply_filters/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { applyFiltersPopover } from './apply_filters_popover'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_group.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_item.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss b/src/plugins/data/public/ui/filter_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss rename to src/plugins/data/public/ui/filter_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss rename to src/plugins/data/public/ui/filter_bar/_variables.scss diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx new file mode 100644 index 0000000000000..2f1b1f8588eb9 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -0,0 +1,205 @@ +/* + * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import classNames from 'classnames'; +import React, { useState } from 'react'; + +import { FilterEditor } from './filter_editor'; +import { FilterItem } from './filter_item'; +import { FilterOptions } from './filter_options'; +import { useKibana } from '../../../../kibana_react/public'; +import { IIndexPattern, esFilters } from '../..'; + +interface Props { + filters: esFilters.Filter[]; + onFiltersUpdated?: (filters: esFilters.Filter[]) => void; + className: string; + indexPatterns: IIndexPattern[]; + intl: InjectedIntl; +} + +function FilterBarUI(props: Props) { + const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); + const kibana = useKibana(); + + const uiSettings = kibana.services.uiSettings; + if (!uiSettings) return null; + + function onFiltersUpdated(filters: esFilters.Filter[]) { + if (props.onFiltersUpdated) { + props.onFiltersUpdated(filters); + } + } + + function renderItems() { + return props.filters.map((filter, i) => ( + + onUpdate(i, newFilter)} + onRemove={() => onRemove(i)} + indexPatterns={props.indexPatterns} + uiSettings={uiSettings!} + /> + + )); + } + + function renderAddFilter() { + const isPinned = uiSettings!.get('filters:pinnedByDefault'); + const [indexPattern] = props.indexPatterns; + const index = indexPattern && indexPattern.id; + const newFilter = esFilters.buildEmptyFilter(isPinned, index); + + const button = ( + setIsAddFilterPopoverOpen(true)} + data-test-subj="addFilter" + className="globalFilterBar__addButton" + > + +{' '} + + + ); + + return ( + + setIsAddFilterPopoverOpen(false)} + anchorPosition="downLeft" + withTitle + panelPaddingSize="none" + ownFocus={true} + > + +
+ setIsAddFilterPopoverOpen(false)} + key={JSON.stringify(newFilter)} + /> +
+
+
+
+ ); + } + + function onAdd(filter: esFilters.Filter) { + setIsAddFilterPopoverOpen(false); + const filters = [...props.filters, filter]; + onFiltersUpdated(filters); + } + + function onRemove(i: number) { + const filters = [...props.filters]; + filters.splice(i, 1); + onFiltersUpdated(filters); + } + + function onUpdate(i: number, filter: esFilters.Filter) { + const filters = [...props.filters]; + filters[i] = filter; + onFiltersUpdated(filters); + } + + function onEnableAll() { + const filters = props.filters.map(esFilters.enableFilter); + onFiltersUpdated(filters); + } + + function onDisableAll() { + const filters = props.filters.map(esFilters.disableFilter); + onFiltersUpdated(filters); + } + + function onPinAll() { + const filters = props.filters.map(esFilters.pinFilter); + onFiltersUpdated(filters); + } + + function onUnpinAll() { + const filters = props.filters.map(esFilters.unpinFilter); + onFiltersUpdated(filters); + } + + function onToggleAllNegated() { + const filters = props.filters.map(esFilters.toggleFilterNegated); + onFiltersUpdated(filters); + } + + function onToggleAllDisabled() { + const filters = props.filters.map(esFilters.toggleFilterDisabled); + onFiltersUpdated(filters); + } + + function onRemoveAll() { + onFiltersUpdated([]); + } + + const classes = classNames('globalFilterBar', props.className); + + return ( + + + + + + + + {renderItems()} + {renderAddFilter()} + + + + ); +} + +export const FilterBar = injectI18n(FilterBarUI); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx new file mode 100644 index 0000000000000..12da4cbab02da --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -0,0 +1,509 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + // @ts-ignore + EuiCodeEditor, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPopoverTitle, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { get } from 'lodash'; +import React, { Component } from 'react'; +import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; +import { + getFieldFromFilter, + getFilterableFields, + getOperatorFromFilter, + getOperatorOptions, + isFilterValid, +} from './lib/filter_editor_utils'; +import { Operator } from './lib/filter_operators'; +import { PhraseValueInput } from './phrase_value_input'; +import { PhrasesValuesInput } from './phrases_values_input'; +import { RangeValueInput } from './range_value_input'; +import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; + +interface Props { + filter: esFilters.Filter; + indexPatterns: IIndexPattern[]; + onSubmit: (filter: esFilters.Filter) => void; + onCancel: () => void; + intl: InjectedIntl; +} + +interface State { + selectedIndexPattern?: IIndexPattern; + selectedField?: IFieldType; + selectedOperator?: Operator; + params: any; + useCustomLabel: boolean; + customLabel: string | null; + queryDsl: string; + isCustomEditorOpen: boolean; +} + +class FilterEditorUI extends Component { + constructor(props: Props) { + super(props); + this.state = { + selectedIndexPattern: this.getIndexPatternFromFilter(), + selectedField: this.getFieldFromFilter(), + selectedOperator: this.getSelectedOperator(), + params: esFilters.getFilterParams(props.filter), + useCustomLabel: props.filter.meta.alias !== null, + customLabel: props.filter.meta.alias, + queryDsl: JSON.stringify(esFilters.cleanFilter(props.filter), null, 2), + isCustomEditorOpen: this.isUnknownFilterType(), + }; + } + + public render() { + return ( +
+ + + + + + + + {this.state.isCustomEditorOpen ? ( + + ) : ( + + )} + + + + + +
+ + {this.renderIndexPatternInput()} + + {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} + + + + + + {this.state.useCustomLabel && ( +
+ + + + +
+ )} + + + + + + + + + + + + + + + + +
+
+
+ ); + } + + private renderIndexPatternInput() { + if ( + this.props.indexPatterns.length <= 1 && + this.props.indexPatterns.find( + indexPattern => indexPattern === this.state.selectedIndexPattern + ) + ) { + return ''; + } + const { selectedIndexPattern } = this.state; + return ( + + + + indexPattern.title} + onChange={this.onIndexPatternChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterIndexPatternsSelect" + /> + + + + ); + } + + private renderRegularEditor() { + return ( +
+ + {this.renderFieldInput()} + + {this.renderOperatorInput()} + + + +
{this.renderParamsEditor()}
+
+ ); + } + + private renderFieldInput() { + const { selectedIndexPattern, selectedField } = this.state; + const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; + + return ( + + field.name} + onChange={this.onFieldChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + className="globalFilterEditor__fieldInput" + data-test-subj="filterFieldSuggestionList" + /> + + ); + } + + private renderOperatorInput() { + const { selectedField, selectedOperator } = this.state; + const operators = selectedField ? getOperatorOptions(selectedField) : []; + return ( + + message} + onChange={this.onOperatorChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterOperatorList" + /> + + ); + } + + private renderCustomEditor() { + return ( + + + + ); + } + + private renderParamsEditor() { + const indexPattern = this.state.selectedIndexPattern; + if (!indexPattern || !this.state.selectedOperator) { + return ''; + } + + switch (this.state.selectedOperator.type) { + case 'exists': + return ''; + case 'phrase': + return ( + + ); + case 'phrases': + return ( + + ); + case 'range': + return ( + + ); + } + } + + private toggleCustomEditor = () => { + const isCustomEditorOpen = !this.state.isCustomEditorOpen; + this.setState({ isCustomEditorOpen }); + }; + + private isUnknownFilterType() { + const { type } = this.props.filter.meta; + return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); + } + + private getIndexPatternFromFilter() { + return utils.getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + } + + private getFieldFromFilter() { + const indexPattern = this.getIndexPatternFromFilter(); + return ( + indexPattern && getFieldFromFilter(this.props.filter as esFilters.FieldFilter, indexPattern) + ); + } + + private getSelectedOperator() { + return getOperatorFromFilter(this.props.filter); + } + + private isFilterValid() { + const { + isCustomEditorOpen, + queryDsl, + selectedIndexPattern: indexPattern, + selectedField: field, + selectedOperator: operator, + params, + } = this.state; + + if (isCustomEditorOpen) { + try { + return Boolean(JSON.parse(queryDsl)); + } catch (e) { + return false; + } + } + + return isFilterValid(indexPattern, field, operator, params); + } + + private onIndexPatternChange = ([selectedIndexPattern]: IIndexPattern[]) => { + const selectedField = undefined; + const selectedOperator = undefined; + const params = undefined; + this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); + }; + + private onFieldChange = ([selectedField]: IFieldType[]) => { + const selectedOperator = undefined; + const params = undefined; + this.setState({ selectedField, selectedOperator, params }); + }; + + private onOperatorChange = ([selectedOperator]: Operator[]) => { + // Only reset params when the operator type changes + const params = + get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') + ? this.state.params + : undefined; + this.setState({ selectedOperator, params }); + }; + + private onCustomLabelSwitchChange = (event: EuiSwitchEvent) => { + const useCustomLabel = event.target.checked; + const customLabel = event.target.checked ? '' : null; + this.setState({ useCustomLabel, customLabel }); + }; + + private onCustomLabelChange = (event: React.ChangeEvent) => { + const customLabel = event.target.value; + this.setState({ customLabel }); + }; + + private onParamsChange = (params: any) => { + this.setState({ params }); + }; + + private onQueryDslChange = (queryDsl: string) => { + this.setState({ queryDsl }); + }; + + private onSubmit = () => { + const { + selectedIndexPattern: indexPattern, + selectedField: field, + selectedOperator: operator, + params, + useCustomLabel, + customLabel, + isCustomEditorOpen, + queryDsl, + } = this.state; + + const { $state } = this.props.filter; + if (!$state || !$state.store) { + return; // typescript validation + } + const alias = useCustomLabel ? customLabel : null; + + if (isCustomEditorOpen) { + const { index, disabled, negate } = this.props.filter.meta; + const newIndex = index || this.props.indexPatterns[0].id!; + const body = JSON.parse(queryDsl); + const filter = esFilters.buildCustomFilter( + newIndex, + body, + disabled, + negate, + alias, + $state.store + ); + this.props.onSubmit(filter); + } else if (indexPattern && field && operator) { + const filter = esFilters.buildFilter( + indexPattern, + field, + operator.type, + operator.negate, + this.props.filter.meta.disabled, + params, + alias, + $state.store + ); + this.props.onSubmit(filter); + } + }; +} + +function IndexPatternComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +function FieldComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +function OperatorComboBox(props: GenericComboBoxProps) { + return GenericComboBox(props); +} + +export const FilterEditor = injectI18n(FilterEditorUI); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts new file mode 100644 index 0000000000000..2cc7f16cfe261 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { + existsFilter, + phraseFilter, + phrasesFilter, + rangeFilter, + stubIndexPattern, + stubFields, +} from '../../../../stubs'; +import { esFilters } from '../../../../index'; +import { + getFieldFromFilter, + getFilterableFields, + getOperatorFromFilter, + getOperatorOptions, + isFilterValid, +} from './filter_editor_utils'; + +import { existsOperator, isBetweenOperator, isOneOfOperator, isOperator } from './filter_operators'; + +jest.mock('ui/new_platform'); + +describe('Filter editor utils', () => { + describe('getFieldFromFilter', () => { + it('should return the field from the filter', () => { + const field = getFieldFromFilter(phraseFilter, stubIndexPattern); + expect(field).not.toBeUndefined(); + expect(field && field.name).toBe(phraseFilter.meta.key); + }); + }); + + describe('getOperatorFromFilter', () => { + it('should return "is" for phrase filter', () => { + const operator = getOperatorFromFilter(phraseFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('phrase'); + expect(operator && operator.negate).toBe(false); + }); + + it('should return "is not" for phrase filter', () => { + const negatedPhraseFilter = esFilters.toggleFilterNegated(phraseFilter); + const operator = getOperatorFromFilter(negatedPhraseFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('phrase'); + expect(operator && operator.negate).toBe(true); + }); + + it('should return "is one of" for phrases filter', () => { + const operator = getOperatorFromFilter(phrasesFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('phrases'); + expect(operator && operator.negate).toBe(false); + }); + + it('should return "is not one of" for negated phrases filter', () => { + const negatedPhrasesFilter = esFilters.toggleFilterNegated(phrasesFilter); + const operator = getOperatorFromFilter(negatedPhrasesFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('phrases'); + expect(operator && operator.negate).toBe(true); + }); + + it('should return "is between" for range filter', () => { + const operator = getOperatorFromFilter(rangeFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('range'); + expect(operator && operator.negate).toBe(false); + }); + + it('should return "is not between" for negated range filter', () => { + const negatedRangeFilter = esFilters.toggleFilterNegated(rangeFilter); + const operator = getOperatorFromFilter(negatedRangeFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('range'); + expect(operator && operator.negate).toBe(true); + }); + + it('should return "exists" for exists filter', () => { + const operator = getOperatorFromFilter(existsFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('exists'); + expect(operator && operator.negate).toBe(false); + }); + + it('should return "does not exists" for negated exists filter', () => { + const negatedExistsFilter = esFilters.toggleFilterNegated(existsFilter); + const operator = getOperatorFromFilter(negatedExistsFilter); + expect(operator).not.toBeUndefined(); + expect(operator && operator.type).toBe('exists'); + expect(operator && operator.negate).toBe(true); + }); + }); + + describe('getFilterableFields', () => { + it('returns the list of fields from the given index pattern', () => { + const fieldOptions = getFilterableFields(stubIndexPattern); + expect(fieldOptions.length).toBeGreaterThan(0); + }); + + it('limits the fields to the filterable fields', () => { + const fieldOptions = getFilterableFields(stubIndexPattern); + const nonFilterableFields = fieldOptions.filter(field => !field.filterable); + expect(nonFilterableFields.length).toBe(0); + }); + }); + + describe('getOperatorOptions', () => { + it('returns range for number fields', () => { + const [field] = stubFields.filter(({ type }) => type === 'number'); + const operatorOptions = getOperatorOptions(field); + const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); + expect(rangeOperator).not.toBeUndefined(); + }); + + it('does not return range for string fields', () => { + const [field] = stubFields.filter(({ type }) => type === 'string'); + const operatorOptions = getOperatorOptions(field); + const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); + expect(rangeOperator).toBeUndefined(); + }); + }); + + describe('isFilterValid', () => { + it('should return false if index pattern is not provided', () => { + const isValid = isFilterValid(undefined, stubFields[0], isOperator, 'foo'); + expect(isValid).toBe(false); + }); + + it('should return false if field is not provided', () => { + const isValid = isFilterValid(stubIndexPattern, undefined, isOperator, 'foo'); + expect(isValid).toBe(false); + }); + + it('should return false if operator is not provided', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], undefined, 'foo'); + expect(isValid).toBe(false); + }); + + it('should return false for phrases filter without phrases', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, []); + expect(isValid).toBe(false); + }); + + it('should return true for phrases filter with phrases', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, ['foo']); + expect(isValid).toBe(true); + }); + + it('should return false for range filter without range', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, undefined); + expect(isValid).toBe(false); + }); + + it('should return true for range filter with from', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { + from: 'foo', + }); + expect(isValid).toBe(true); + }); + + it('should return true for range filter with from/to', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { + from: 'foo', + too: 'goo', + }); + expect(isValid).toBe(true); + }); + + it('should return true for exists filter without params', () => { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], existsOperator); + expect(isValid).toBe(true); + }); + }); +}); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts new file mode 100644 index 0000000000000..422ffb162125d --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -0,0 +1,88 @@ +/* + * 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 dateMath from '@elastic/datemath'; +import { Ipv4Address } from '../../../../../../kibana_utils/public'; +import { FILTER_OPERATORS, Operator } from './filter_operators'; +import { esFilters, IIndexPattern, IFieldType, isFilterable } from '../../../..'; + +export function getFieldFromFilter(filter: esFilters.FieldFilter, indexPattern: IIndexPattern) { + return indexPattern.fields.find(field => field.name === filter.meta.key); +} + +export function getOperatorFromFilter(filter: esFilters.Filter) { + return FILTER_OPERATORS.find(operator => { + return filter.meta.type === operator.type && filter.meta.negate === operator.negate; + }); +} + +export function getFilterableFields(indexPattern: IIndexPattern) { + return indexPattern.fields.filter(isFilterable); +} + +export function getOperatorOptions(field: IFieldType) { + return FILTER_OPERATORS.filter(operator => { + return !operator.fieldTypes || operator.fieldTypes.includes(field.type); + }); +} + +export function validateParams(params: any, type: string) { + switch (type) { + case 'date': + const moment = typeof params === 'string' ? dateMath.parse(params) : null; + return Boolean(typeof params === 'string' && moment && moment.isValid()); + case 'ip': + try { + return Boolean(new Ipv4Address(params)); + } catch (e) { + return false; + } + default: + return true; + } +} + +export function isFilterValid( + indexPattern?: IIndexPattern, + field?: IFieldType, + operator?: Operator, + params?: any +) { + if (!indexPattern || !field || !operator) { + return false; + } + switch (operator.type) { + case 'phrase': + return validateParams(params, field.type); + case 'phrases': + if (!Array.isArray(params) || !params.length) { + return false; + } + return params.every(phrase => validateParams(phrase, field.type)); + case 'range': + if (typeof params !== 'object') { + return false; + } + return validateParams(params.from, field.type) || validateParams(params.to, field.type); + case 'exists': + return true; + default: + throw new Error(`Unknown operator type: ${operator.type}`); + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js index 042a353031c33..3eb46645522e1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js @@ -19,8 +19,8 @@ import React from 'react'; import { FilterLabel } from './filter_label'; -import { phraseFilter } from './fixtures/phrase_filter'; import { shallow } from 'enzyme'; +import { phraseFilter } from '../../../../stubs'; test('alias', () => { const filter = { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx similarity index 87% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index d16158226579c..49a0d6f2ab3bd 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -21,7 +21,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; interface Props { filter: esFilters.Filter; @@ -51,50 +51,43 @@ export function FilterLabel({ filter, valueLabel }: Props) { } switch (filter.meta.type) { - case 'exists': + case esFilters.FILTERS.EXISTS: return ( {prefix} {filter.meta.key} {existsOperator.message} ); - case 'geo_bounding_box': + case esFilters.FILTERS.GEO_BOUNDING_BOX: return ( {prefix} {filter.meta.key}: {valueLabel} ); - case 'geo_polygon': + case esFilters.FILTERS.GEO_POLYGON: return ( {prefix} {filter.meta.key}: {valueLabel} ); - case 'phrase': - return ( - - {prefix} - {filter.meta.key}: {valueLabel} - - ); - case 'phrases': + case esFilters.FILTERS.PHRASES: return ( {prefix} {filter.meta.key} {isOneOfOperator.message} {valueLabel} ); - case 'query_string': + case esFilters.FILTERS.QUERY_STRING: return ( {prefix} {valueLabel} ); - case 'range': - case 'phrase': + case esFilters.FILTERS.PHRASE: + case esFilters.FILTERS.RANGE: return ( {prefix} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts similarity index 89% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts index 469f5355df106..bb15cffa67b59 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts @@ -18,10 +18,11 @@ */ import { i18n } from '@kbn/i18n'; +import { esFilters } from '../../../..'; export interface Operator { message: string; - type: string; + type: esFilters.FILTERS; negate: boolean; fieldTypes?: string[]; } @@ -30,7 +31,7 @@ export const isOperator = { message: i18n.translate('data.filter.filterEditor.isOperatorOptionLabel', { defaultMessage: 'is', }), - type: 'phrase', + type: esFilters.FILTERS.PHRASE, negate: false, }; @@ -38,7 +39,7 @@ export const isNotOperator = { message: i18n.translate('data.filter.filterEditor.isNotOperatorOptionLabel', { defaultMessage: 'is not', }), - type: 'phrase', + type: esFilters.FILTERS.PHRASE, negate: true, }; @@ -46,7 +47,7 @@ export const isOneOfOperator = { message: i18n.translate('data.filter.filterEditor.isOneOfOperatorOptionLabel', { defaultMessage: 'is one of', }), - type: 'phrases', + type: esFilters.FILTERS.PHRASES, negate: false, fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], }; @@ -55,7 +56,7 @@ export const isNotOneOfOperator = { message: i18n.translate('data.filter.filterEditor.isNotOneOfOperatorOptionLabel', { defaultMessage: 'is not one of', }), - type: 'phrases', + type: esFilters.FILTERS.PHRASES, negate: true, fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], }; @@ -64,7 +65,7 @@ export const isBetweenOperator = { message: i18n.translate('data.filter.filterEditor.isBetweenOperatorOptionLabel', { defaultMessage: 'is between', }), - type: 'range', + type: esFilters.FILTERS.RANGE, negate: false, fieldTypes: ['number', 'date', 'ip'], }; @@ -73,7 +74,7 @@ export const isNotBetweenOperator = { message: i18n.translate('data.filter.filterEditor.isNotBetweenOperatorOptionLabel', { defaultMessage: 'is not between', }), - type: 'range', + type: esFilters.FILTERS.RANGE, negate: true, fieldTypes: ['number', 'date', 'ip'], }; @@ -82,7 +83,7 @@ export const existsOperator = { message: i18n.translate('data.filter.filterEditor.existsOperatorOptionLabel', { defaultMessage: 'exists', }), - type: 'exists', + type: esFilters.FILTERS.EXISTS, negate: false, }; @@ -90,7 +91,7 @@ export const doesNotExistOperator = { message: i18n.translate('data.filter.filterEditor.doesNotExistOperatorOptionLabel', { defaultMessage: 'does not exist', }), - type: 'exists', + type: esFilters.FILTERS.EXISTS, negate: true, }; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx similarity index 90% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 426c21c99ccdb..61290cc16b8a8 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -19,17 +19,14 @@ import { Component } from 'react'; import { debounce } from 'lodash'; -import { Field, IndexPattern } from '../../../index_patterns'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; -import { IDataPluginServices } from '../../../types'; + +import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; +import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; - indexPattern: IndexPattern; - field?: Field; + indexPattern: IIndexPattern; + field?: IFieldType; } export interface PhraseSuggestorState { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx index 7ef51f88ba57e..b16994cb0057b 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx @@ -24,7 +24,7 @@ import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { value?: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx index f3b30e2ad5fd9..aa76684239b63 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx @@ -23,7 +23,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { values?: string[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx index 6a5229ac826cb..65b842f0bd4aa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx @@ -21,8 +21,8 @@ import { EuiIcon, EuiLink, EuiFormHelpText, EuiFormControlLayoutDelimited } from import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React from 'react'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; -import { Field } from '../../../index_patterns'; +import { useKibana } from '../../../../../kibana_react/public'; +import { IFieldType } from '../../..'; import { ValueInputType } from './value_input_type'; interface RangeParams { @@ -33,7 +33,7 @@ interface RangeParams { type RangeParamsPartial = Partial; interface Props { - field?: Field; + field?: IFieldType; value?: RangeParams; onChange: (params: RangeParamsPartial) => void; intl: InjectedIntl; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx similarity index 95% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx rename to src/plugins/data/public/ui/filter_bar/filter_item.tsx index 0dbe92dcb0da6..4ef0b2740e5fa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -22,16 +22,14 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import { UiSettingsClientContract } from 'src/core/public'; -import { IndexPattern } from '../../index_patterns'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { getDisplayValueFromFilter } from './filter_editor/lib/get_display_value'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern } from '../..'; interface Props { id: string; filter: esFilters.Filter; - indexPatterns: IndexPattern[]; + indexPatterns: IIndexPattern[]; className?: string; onUpdate: (filter: esFilters.Filter) => void; onRemove: () => void; @@ -62,7 +60,7 @@ class FilterItemUI extends Component { this.props.className ); - const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; const dataTestSubjDisabled = `filter-${ diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx rename to src/plugins/data/public/ui/filter_bar/filter_options.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx new file mode 100644 index 0000000000000..dd12789d15a9d --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.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 { EuiBadge, useInnerText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC } from 'react'; +import { FilterLabel } from '../filter_editor/lib/filter_label'; +import { esFilters } from '../../..'; + +interface Props { + filter: esFilters.Filter; + valueLabel: string; + [propName: string]: any; +} + +export const FilterView: FC = ({ + filter, + iconOnClick, + onClick, + valueLabel, + ...rest +}: Props) => { + const [ref, innerText] = useInnerText(); + + let title = i18n.translate('data.filter.filterBar.moreFilterActionsMessage', { + defaultMessage: 'Filter: {innerText}. Select for more filter actions.', + values: { innerText }, + }); + + if (esFilters.isFilterPinned(filter)) { + title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', { + defaultMessage: 'Pinned', + })} ${title}`; + } + if (filter.meta.disabled) { + title = `${i18n.translate('data.filter.filterBar.disabledFilterPrefix', { + defaultMessage: 'Disabled', + })} ${title}`; + } + + return ( + + + + + + ); +}; diff --git a/src/plugins/data/public/ui/filter_bar/index.ts b/src/plugins/data/public/ui/filter_bar/index.ts new file mode 100644 index 0000000000000..b975317d46630 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FilterBar } from './filter_bar'; +export { FilterLabel } from './filter_editor/lib/filter_label'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts new file mode 100644 index 0000000000000..cb7c92b00ea3a --- /dev/null +++ b/src/plugins/data/public/ui/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { IndexPatternSelect } from './index_pattern_select'; +export { FilterBar } from './filter_bar'; +export { applyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index.ts b/src/plugins/data/public/ui/index_pattern_select/index.ts similarity index 100% rename from src/legacy/core_plugins/data/public/index_patterns/components/index.ts rename to src/plugins/data/public/ui/index_pattern_select/index.ts diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx rename to src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 77692d7bcaa0d..f868e4b1f7504 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -21,10 +21,10 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../../core/public'; -import { getIndexPatternTitle } from '../utils'; +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { getIndexPatternTitle } from '../../index_patterns/lib'; -interface IndexPatternSelectProps { +export interface IndexPatternSelectProps { onChange: (opt: any) => void; indexPatternId: string; placeholder: string; diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index 32570a05031f6..cc13269e1aa21 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -55,7 +55,7 @@ describe('createApi', () => { }); it('should throw if no provider is found for the given name', () => { - expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( + expect(api.search({}, {}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( `"No strategy found for noneByThisName"` ); }); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 4c13dd9e1137c..2a874869526d7 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -30,7 +30,7 @@ export function createApi({ caller: APICaller; }) { const api: IRouteHandlerSearchContext = { - search: async (request, strategyName) => { + search: async (request, options, strategyName) => { const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 619a28df839bd..7b725a47aa13b 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -66,7 +66,7 @@ describe('ES search strategy', () => { expect(spy).toBeCalled(); }); - it('calls the API caller with the params', () => { + it('calls the API caller with the params with defaults', () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { @@ -80,7 +80,31 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); - expect(mockApiCaller.mock.calls[0][1]).toEqual(params); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + ignoreUnavailable: true, + restTotalHitsAsInt: true, + }); + }); + + it('calls the API caller with overridden defaults', () => { + const params = { index: 'logstash-*', ignoreUnavailable: false }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + esSearch.search({ params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('search'); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + restTotalHitsAsInt: true, + }); }); it('returns total, loaded, and raw response', async () => { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 31f4fc15a0989..c5fc1d9d3a11c 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search'; +import { ES_SEARCH_STRATEGY } from '../../../common/search'; import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; import { ISearchContext } from '..'; @@ -27,16 +27,17 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { - search: async (request: IEsSearchRequest) => { + search: async (request, options) => { + const params = { + ignoreUnavailable: true, // Don't fail if the index/indices don't exist + restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range + ...request.params, + }; if (request.debug) { // eslint-disable-next-line - console.log(JSON.stringify(request, null, 2)); + console.log(JSON.stringify(params, null, 2)); } - const esSearchResponse = (await caller('search', { - ...request.params, - // TODO: could do something like this here? - // ...getCurrentSearchParams(context), - })) as SearchResponse; + const esSearchResponse = (await caller('search', params, options)) as SearchResponse; // The above query will either complete or timeout and throw an error. // There is no progress indication on this api. diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index fabcb98ceea72..0a35734574153 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -22,6 +22,10 @@ import { TStrategyTypes } from './strategy_types'; import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; import { IEsSearchRequest } from './es_search'; +export interface ISearchOptions { + signal?: AbortSignal; +} + export interface IRequestTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchRequest; [key: string]: IKibanaSearchRequest; @@ -34,9 +38,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], + options?: ISearchOptions, strategy?: T ) => Promise; export type ISearch = ( - request: IRequestTypesMap[T] + request: IRequestTypesMap[T], + options?: ISearchOptions ) => Promise; diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index ebdcf48f608b9..a2394d88f3931 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -60,7 +60,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); @@ -92,7 +92,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.internalError).toBeCalled(); expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6cb6c28c76014..eaa72548e08ee 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -36,7 +36,7 @@ export function registerSearchRoute(router: IRouter): void { const searchRequest = request.body; const strategy = request.params.strategy; try { - const response = await context.search!.search(searchRequest, strategy); + const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { return res.internalError({ body: err }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 4edb51300dfaf..3409a72326121 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -77,7 +77,7 @@ export class SearchService implements Plugin { caller, searchStrategies: this.searchStrategies, }); - return searchAPI.search(request, strategyName); + return searchAPI.search(request, {}, strategyName); }, }, }; diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index 307035c7ec664..df7b1e3a781b0 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -1,6 +1,7 @@ { - "id": "devTools", + "id": "dev_tools", "version": "kibana", "server": false, - "ui": true + "ui": true, + "requiredPlugins": ["kibana_legacy"] } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx b/src/plugins/dev_tools/public/application.tsx similarity index 94% rename from src/legacy/core_plugins/kibana/public/dev_tools/application.tsx rename to src/plugins/dev_tools/public/application.tsx index 3945d8d8dc856..b3c6bb592f378 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -26,7 +26,7 @@ import ReactDOM from 'react-dom'; import { useEffect, useRef } from 'react'; import { AppMountContext } from 'kibana/public'; -import { DevTool } from '../../../../../plugins/dev_tools/public'; +import { DevTool } from './plugin'; interface DevToolsWrapperProps { devTools: readonly DevTool[]; @@ -120,10 +120,10 @@ function setBadge(appMountContext: AppMountContext) { return; } appMountContext.core.chrome.setBadge({ - text: i18n.translate('kbn.devTools.badge.readOnly.text', { + text: i18n.translate('devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n.translate('kbn.devTools.badge.readOnly.tooltip', { + tooltip: i18n.translate('devTools.badge.readOnly.tooltip', { defaultMessage: 'Unable to save', }), iconType: 'glasses', @@ -133,7 +133,7 @@ function setBadge(appMountContext: AppMountContext) { function setBreadcrumbs(appMountContext: AppMountContext) { appMountContext.core.chrome.setBreadcrumbs([ { - text: i18n.translate('kbn.devTools.k7BreadcrumbsDevToolsLabel', { + text: i18n.translate('devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools', }), href: '#/dev_tools', diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 8098308c0882b..124c00c755904 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -19,6 +19,7 @@ import { App, CoreSetup, Plugin } from 'kibana/public'; import { sortBy } from 'lodash'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; export interface DevToolsSetup { /** @@ -93,7 +94,24 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { kibana_legacy }: { kibana_legacy: KibanaLegacySetup }) { + kibana_legacy.registerLegacyApp({ + id: 'dev_tools', + title: 'Dev Tools', + mount: async (appMountContext, params) => { + if (!this.getSortedDevTools) { + throw new Error('not started yet'); + } + const { renderApp } = await import('./application'); + return renderApp( + params.element, + appMountContext, + params.appBasePath, + this.getSortedDevTools() + ); + }, + }); + return { register: (devTool: DevTool) => { if (this.devTools.has(devTool.id)) { diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 33855b07df7a1..ea2bd910b0624 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -61,5 +61,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { EmbeddablePublicPlugin as Plugin }; -export * from './plugin'; +export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index ef1517bb7f1d5..fd299bc626fb9 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,14 +17,15 @@ * under the License. */ -import { Plugin } from '.'; +import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked>; -export type Start = jest.Mocked>; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -43,7 +44,7 @@ const createStartContract = (): Start => { }; const createInstance = () => { - const plugin = new Plugin({} as any); + const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: uiActionsPluginMock.createSetupContract(), }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 458c8bfeb8762..df1f4e5080031 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -27,7 +27,13 @@ export interface IEmbeddableSetupDependencies { uiActions: IUiActionsSetup; } -export class EmbeddablePublicPlugin implements Plugin { +export interface IEmbeddableSetup { + registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +} + +export type IEmbeddableStart = EmbeddableApi; + +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private api!: EmbeddableApi; @@ -52,6 +58,3 @@ export class EmbeddablePublicPlugin implements Plugin { public stop() {} } - -export type Setup = ReturnType; -export type Start = ReturnType; diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 5b50bddefcdb7..6d1e15137480a 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -21,14 +21,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; // eslint-disable-next-line import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests'; import { IUiActionsApi } from 'src/plugins/ui_actions/public'; -import { EmbeddablePublicPlugin } from '../plugin'; +import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: ReturnType; - doStart: (anotherCoreStart?: CoreStart) => ReturnType; + setup: IEmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; uiActions: IUiActionsApi; } diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 3f4050e98f64d..89dea53d75b38 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -29,20 +29,26 @@ interface Props { import { TextField, + TextAreaField, NumericField, CheckBoxField, ComboBoxField, MultiSelectField, + RadioGroupField, + RangeField, SelectField, ToggleField, } from './fields'; const mapTypeToFieldComponent = { [FIELD_TYPES.TEXT]: TextField, + [FIELD_TYPES.TEXTAREA]: TextAreaField, [FIELD_TYPES.NUMBER]: NumericField, [FIELD_TYPES.CHECKBOX]: CheckBoxField, [FIELD_TYPES.COMBO_BOX]: ComboBoxField, [FIELD_TYPES.MULTI_SELECT]: MultiSelectField, + [FIELD_TYPES.RADIO_GROUP]: RadioGroupField, + [FIELD_TYPES.RANGE]: RangeField, [FIELD_TYPES.SELECT]: SelectField, [FIELD_TYPES.TOGGLE]: ToggleField, }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index 73e8c1ff1426b..0443b4ff09e60 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { EuiFormRow, EuiCheckbox } from '@elastic/eui'; import uuid from 'uuid'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 5c2e4a4165d5f..fa73e5a663863 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -82,6 +82,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => return ( ; + idAria?: string; + [key: string]: any; +} + +export const RadioGroupField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx new file mode 100644 index 0000000000000..4ed2dd40968e5 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx @@ -0,0 +1,68 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiFormRow, EuiRange } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; + +interface Props { + field: FieldHook; + euiFieldProps?: Record; + idAria?: string; + [key: string]: any; +} + +export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChange = useCallback( + (e: React.ChangeEvent | React.MouseEvent) => { + const event = ({ ...e, value: `${e.currentTarget.value}` } as unknown) as React.ChangeEvent<{ + value: string; + }>; + field.onChange(event); + }, + [field.onChange] + ); + + return ( + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx index b7eb1c5fa3bd3..a6d77e3b179ed 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; @@ -49,10 +48,11 @@ export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { onChange={e => { field.setValue(e.target.value); }} + options={[]} hasNoInitialSelection={true} isInvalid={isInvalid} data-test-subj="select" - {...(euiFieldProps as { options: any; [key: string]: any })} + {...euiFieldProps} /> ); diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx index 6916f224f8bda..b9c6424a00656 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiTextArea } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx index 6e1bc639e65ce..9e255d8eda22c 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx index 0c075c497a4d0..c6d89d0bfde21 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/helpers.ts b/src/plugins/es_ui_shared/static/forms/components/helpers.ts deleted file mode 100644 index a7543d31bb547..0000000000000 --- a/src/plugins/es_ui_shared/static/forms/components/helpers.ts +++ /dev/null @@ -1,30 +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 { FieldHook } from '../hook_form_lib'; - -export const getFieldValidityAndErrorMessage = ( - field: FieldHook -): { isInvalid: boolean; errorMessage: string | null } => { - const isInvalid = !field.isChangingValue && field.errors.length > 0; - const errorMessage = - !field.isChangingValue && field.errors.length ? field.errors[0].message : null; - - return { isInvalid, errorMessage }; -}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index f65b7cd0aa0b0..df2807e59ab46 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -20,10 +20,13 @@ // Field types export const FIELD_TYPES = { TEXT: 'text', + TEXTAREA: 'textarea', NUMBER: 'number', TOGGLE: 'toggle', CHECKBOX: 'checkbox', COMBO_BOX: 'comboBox', + RADIO_GROUP: 'radioGroup', + RANGE: 'range', SELECT: 'select', MULTI_SELECT: 'multiSelect', }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts new file mode 100644 index 0000000000000..e71d52d6ff003 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FieldHook } from './types'; + +export const getFieldValidityAndErrorMessage = ( + field: FieldHook +): { isInvalid: boolean; errorMessage: string | null } => { + const isInvalid = !field.isChangingValue && field.errors.length > 0; + const errorMessage = + !field.isChangingValue && field.errors.length ? field.errors[0].message : null; + + return { isInvalid, errorMessage }; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 8a1012404b377..d7ef798bf2e03 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -27,6 +27,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) type = FIELD_TYPES.TEXT, defaultValue = '', label = '', + labelAppend = '', helpText = '', validations = [], formatters = [], @@ -382,6 +383,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) path, type, label, + labelAppend, helpText, value, errors, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 6e1a5b075d318..3079814c9ad14 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -20,6 +20,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component export { useForm } from './hooks'; +export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; export * from './components'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 28e2a346bd5c4..9946020132354 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -71,6 +71,7 @@ export interface FormOptions { export interface FieldHook { readonly path: string; readonly label?: string; + readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type: string; readonly value: unknown; @@ -98,6 +99,7 @@ export interface FieldHook { export interface FieldConfig { readonly path?: string; readonly label?: string; + readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type?: HTMLInputElement['type']; readonly defaultValue?: unknown; diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index a14aaae98fc34..6dc88fd23f29a 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -20,10 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ExpressionsPublicPlugin(initializerContext); -} - export { ExpressionsPublicPlugin as Plugin }; export * from './plugin'; @@ -31,3 +27,10 @@ export * from './types'; export * from '../common'; export { interpreterProvider, ExpressionInterpret } from './interpreter_provider'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionDataHandler } from './execute'; + +export { RenderResult, ExpressionRenderHandler } from './render'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsPublicPlugin(initializerContext); +} diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 0b5c62d947564..1a9c878c8808d 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -17,8 +17,8 @@ * under the License. */ +import { first, skip } from 'rxjs/operators'; import { fromExpression } from '@kbn/interpreter/common'; -import { first } from 'rxjs/operators'; import { loader, ExpressionLoader } from './loader'; import { ExpressionDataHandler } from './execute'; import { IInterpreterRenderHandlers } from './types'; @@ -124,7 +124,7 @@ describe('ExpressionLoader', () => { let response = await expressionLoader.render$.pipe(first()).toPromise(); expect(response).toBe(1); expressionLoader.update('test'); - response = await expressionLoader.render$.pipe(first()).toPromise(); + response = await expressionLoader.render$.pipe(skip(1), first()).toPromise(); expect(response).toBe(2); }); diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 1e26c813a8902..200249b60c773 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -39,7 +39,6 @@ export class ExpressionLoader { private loadingSubject: Subject; private data: Data; private params: IExpressionLoaderParams = {}; - private ignoreNextResponse = false; constructor( element: HTMLElement, @@ -134,15 +133,14 @@ export class ExpressionLoader { params: IExpressionLoaderParams ): Promise => { if (this.dataHandler && this.dataHandler.isPending) { - this.ignoreNextResponse = true; this.dataHandler.cancel(); } this.setParams(params); this.dataHandler = new ExpressionDataHandler(expression, params); if (!params.inspectorAdapters) params.inspectorAdapters = this.dataHandler.inspect(); - const data = await this.dataHandler.getData(); - if (this.ignoreNextResponse) { - this.ignoreNextResponse = false; + const prevDataHandler = this.dataHandler; + const data = await prevDataHandler.getData(); + if (this.dataHandler !== prevDataHandler) { return; } this.dataSubject.next(data); diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts index 9d555f9760ee7..6b5acc8405fd2 100644 --- a/src/plugins/expressions/public/render.test.ts +++ b/src/plugins/expressions/public/render.test.ts @@ -128,5 +128,19 @@ describe('ExpressionRenderHandler', () => { expressionRenderHandler.render({ type: 'render', as: 'test' }); }); }); + + // in case render$ subscription happen after render() got called + // we still want to be notified about sync render$ updates + it("doesn't swallow sync render errors", async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + expressionRenderHandler.render(false); + const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); + await expect(promise).resolves.toEqual({ + type: 'error', + error: { + message: 'invalid data provided to the expression renderer', + }, + }); + }); }); }); diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 8475325a2c625..3c7008806e779 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -19,7 +19,7 @@ import { Observable } from 'rxjs'; import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; +import { filter, share } from 'rxjs/operators'; import { event, RenderId, Data, IInterpreterRenderHandlers } from './types'; import { getRenderersRegistry } from './services'; @@ -30,15 +30,17 @@ interface RenderError { export type IExpressionRendererExtraHandlers = Record; +export type RenderResult = RenderId | RenderError; + export class ExpressionRenderHandler { - render$: Observable; + render$: Observable; update$: Observable; events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.Subject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; @@ -49,8 +51,11 @@ export class ExpressionRenderHandler { this.eventsSubject = new Rx.Subject(); this.events$ = this.eventsSubject.asObservable().pipe(share()); - this.renderSubject = new Rx.Subject(); - this.render$ = this.renderSubject.asObservable().pipe(share()); + this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null); + this.render$ = this.renderSubject.asObservable().pipe( + share(), + filter(_ => _ !== null) + ) as Observable; this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable().pipe(share()); @@ -75,7 +80,7 @@ export class ExpressionRenderHandler { }; } - render = (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { + render = async (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { if (!data || typeof data !== 'object') { this.renderSubject.next({ type: 'error', @@ -108,7 +113,7 @@ export class ExpressionRenderHandler { try { // Rendering is asynchronous, completed by handlers.done() - getRenderersRegistry() + await getRenderersRegistry() .get(data.as)! .render(this.element, data.value, { ...this.handlers, ...extraHandlers }); } catch (e) { diff --git a/src/plugins/feature_catalogue/README.md b/src/plugins/feature_catalogue/README.md deleted file mode 100644 index 68584e7ed2ce1..0000000000000 --- a/src/plugins/feature_catalogue/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Feature catalogue plugin - -Replaces the legacy `ui/registry/feature_catalogue` module for registering "features" that should be showed in the home -page's feature catalogue. This should not be confused with the "feature" plugin for registering features used to derive -UI capabilities for feature controls. - -## Example registration - -```ts -// For legacy plugins -import { npSetup } from 'ui/new_platform'; -npSetup.plugins.feature_catalogue.register(/* same details here */); - -// For new plugins: first add 'feature_catalogue` to the list of `optionalPlugins` -// in your kibana.json file. Then access the plugin directly in `setup`: - -class MyPlugin { - setup(core, plugins) { - if (plugins.feature_catalogue) { - plugins.feature_catalogue.register(/* same details here. */); - } - } -} -``` - -Note that the old module supported providing a Angular DI function to receive Angular dependencies. This is no longer supported as we migrate away from Angular and will be removed in 8.0. diff --git a/src/plugins/feature_catalogue/kibana.json b/src/plugins/feature_catalogue/kibana.json deleted file mode 100644 index 3f39c9361f047..0000000000000 --- a/src/plugins/feature_catalogue/kibana.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "feature_catalogue", - "version": "kibana", - "server": false, - "ui": true -} diff --git a/src/plugins/feature_catalogue/public/index.ts b/src/plugins/feature_catalogue/public/index.ts deleted file mode 100644 index dd241a317c4a6..0000000000000 --- a/src/plugins/feature_catalogue/public/index.ts +++ /dev/null @@ -1,24 +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. - */ - -export { FeatureCatalogueSetup, FeatureCatalogueStart } from './plugin'; -export { FeatureCatalogueEntry, FeatureCatalogueCategory } from './services'; -import { FeatureCataloguePlugin } from './plugin'; - -export const plugin = () => new FeatureCataloguePlugin(); diff --git a/src/plugins/feature_catalogue/public/plugin.ts b/src/plugins/feature_catalogue/public/plugin.ts deleted file mode 100644 index 46a70baff488a..0000000000000 --- a/src/plugins/feature_catalogue/public/plugin.ts +++ /dev/null @@ -1,50 +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 { CoreStart, Plugin } from 'src/core/public'; -import { - FeatureCatalogueRegistry, - FeatureCatalogueRegistrySetup, - FeatureCatalogueRegistryStart, -} from './services'; - -export class FeatureCataloguePlugin - implements Plugin { - private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); - - public async setup() { - return { - ...this.featuresCatalogueRegistry.setup(), - }; - } - - public async start(core: CoreStart) { - return { - ...this.featuresCatalogueRegistry.start({ - capabilities: core.application.capabilities, - }), - }; - } -} - -/** @public */ -export type FeatureCatalogueSetup = FeatureCatalogueRegistrySetup; - -/** @public */ -export type FeatureCatalogueStart = FeatureCatalogueRegistryStart; diff --git a/src/plugins/feature_catalogue/public/services/index.ts b/src/plugins/feature_catalogue/public/services/index.ts deleted file mode 100644 index 17433264f5a42..0000000000000 --- a/src/plugins/feature_catalogue/public/services/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './feature_catalogue_registry'; diff --git a/src/plugins/home/README.md b/src/plugins/home/README.md new file mode 100644 index 0000000000000..74e12a799b1b7 --- /dev/null +++ b/src/plugins/home/README.md @@ -0,0 +1,29 @@ +# home plugin +Moves the legacy `ui/registry/feature_catalogue` module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + +# Feature catalogue (public service) + +Replaces the legacy `ui/registry/feature_catalogue` module for registering "features" that should be showed in the home +page's feature catalogue. This should not be confused with the "feature" plugin for registering features used to derive +UI capabilities for feature controls. + +## Example registration + +```ts +// For legacy plugins +import { npSetup } from 'ui/new_platform'; +npSetup.plugins.home.featureCatalogue.register(/* same details here */); + +// For new plugins: first add 'home` to the list of `optionalPlugins` +// in your kibana.json file. Then access the plugin directly in `setup`: + +class MyPlugin { + setup(core, plugins) { + if (plugins.home) { + plugins.home.featureCatalgoue.register(/* same details here. */); + } + } +} +``` + +Note that the old module supported providing a Angular DI function to receive Angular dependencies. This is no longer supported as we migrate away from Angular and will be removed in 8.0. diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index 8d2d79560f854..a5c65e3efa597 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -2,5 +2,5 @@ "id": "home", "version": "kibana", "server": true, - "ui": false + "ui": true } diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts new file mode 100644 index 0000000000000..25e94c20c347b --- /dev/null +++ b/src/plugins/home/public/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + FeatureCatalogueSetup, + FeatureCatalogueStart, + HomePublicPluginSetup, + HomePublicPluginStart, +} from './plugin'; +export { FeatureCatalogueEntry, FeatureCatalogueCategory } from './services'; +import { HomePublicPlugin } from './plugin'; + +export const plugin = () => new HomePublicPlugin(); diff --git a/src/plugins/feature_catalogue/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts similarity index 95% rename from src/plugins/feature_catalogue/public/plugin.test.mocks.ts rename to src/plugins/home/public/plugin.test.mocks.ts index c0da6a179204b..a48ea8f795136 100644 --- a/src/plugins/feature_catalogue/public/plugin.test.mocks.ts +++ b/src/plugins/home/public/plugin.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { featureCatalogueRegistryMock } from './services/feature_catalogue_registry.mock'; +import { featureCatalogueRegistryMock } from './services/feature_catalogue/feature_catalogue_registry.mock'; export const registryMock = featureCatalogueRegistryMock.create(); jest.doMock('./services', () => ({ diff --git a/src/plugins/feature_catalogue/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts similarity index 79% rename from src/plugins/feature_catalogue/public/plugin.test.ts rename to src/plugins/home/public/plugin.test.ts index 8bbbb973b459e..fad6e8cf47bfe 100644 --- a/src/plugins/feature_catalogue/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -18,9 +18,9 @@ */ import { registryMock } from './plugin.test.mocks'; -import { FeatureCataloguePlugin } from './plugin'; +import { HomePublicPlugin } from './plugin'; -describe('FeatureCataloguePlugin', () => { +describe('HomePublicPlugin', () => { beforeEach(() => { registryMock.setup.mockClear(); registryMock.start.mockClear(); @@ -28,22 +28,22 @@ describe('FeatureCataloguePlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new FeatureCataloguePlugin().setup(); - expect(registryMock.setup).toHaveBeenCalledWith(); - expect(setup.register).toBeDefined(); + const setup = await new HomePublicPlugin().setup(); + expect(setup).toHaveProperty('featureCatalogue'); + expect(setup.featureCatalogue).toHaveProperty('register'); }); }); describe('start', () => { test('wires up and returns registry', async () => { - const service = new FeatureCataloguePlugin(); + const service = new HomePublicPlugin(); await service.setup(); const core = { application: { capabilities: { catalogue: {} } } } as any; const start = await service.start(core); expect(registryMock.start).toHaveBeenCalledWith({ capabilities: core.application.capabilities, }); - expect(start.get).toBeDefined(); + expect(start.featureCatalogue.get).toBeDefined(); }); }); }); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts new file mode 100644 index 0000000000000..40f2047ef0016 --- /dev/null +++ b/src/plugins/home/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart, Plugin } from 'src/core/public'; +import { + FeatureCatalogueRegistry, + FeatureCatalogueRegistrySetup, + FeatureCatalogueRegistryStart, +} from './services'; + +export class HomePublicPlugin implements Plugin { + private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); + + public async setup() { + return { + featureCatalogue: { ...this.featuresCatalogueRegistry.setup() }, + }; + } + + public async start(core: CoreStart) { + return { + featureCatalogue: { + ...this.featuresCatalogueRegistry.start({ + capabilities: core.application.capabilities, + }), + }, + }; + } +} + +/** @public */ +export type FeatureCatalogueSetup = FeatureCatalogueRegistrySetup; + +/** @public */ +export type FeatureCatalogueStart = FeatureCatalogueRegistryStart; + +/** @public */ +export interface HomePublicPluginSetup { + featureCatalogue: FeatureCatalogueSetup; +} + +/** @public */ +export interface HomePublicPluginStart { + featureCatalogue: FeatureCatalogueStart; +} diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts similarity index 100% rename from src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts rename to src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts similarity index 100% rename from src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts rename to src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts similarity index 100% rename from src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts rename to src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts diff --git a/src/plugins/home/public/services/feature_catalogue/index.ts b/src/plugins/home/public/services/feature_catalogue/index.ts new file mode 100644 index 0000000000000..eae01271e8559 --- /dev/null +++ b/src/plugins/home/public/services/feature_catalogue/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + FeatureCatalogueCategory, + FeatureCatalogueEntry, + FeatureCatalogueRegistry, + FeatureCatalogueRegistrySetup, + FeatureCatalogueRegistryStart, +} from './feature_catalogue_registry'; diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts new file mode 100644 index 0000000000000..3621b0912393a --- /dev/null +++ b/src/plugins/home/public/services/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './feature_catalogue'; diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 178a77dc85ca9..be4e20ab63d3c 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -17,8 +17,8 @@ * under the License. */ -export { HomePluginSetup, HomePluginStart } from './plugin'; +export { HomeServerPluginSetup, HomeServerPluginStart } from './plugin'; export { TutorialProvider } from './services'; -import { HomePlugin } from './plugin'; +import { HomeServerPlugin } from './plugin'; -export const plugin = () => new HomePlugin(); +export const plugin = () => new HomeServerPlugin(); diff --git a/src/plugins/home/server/plugin.test.mocks.ts b/src/plugins/home/server/plugin.test.mocks.ts index df63b467d8656..a5640de579b15 100644 --- a/src/plugins/home/server/plugin.test.mocks.ts +++ b/src/plugins/home/server/plugin.test.mocks.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { tutorialsRegistryMock } from './services/tutorials_registry.mock'; +import { tutorialsRegistryMock } from './services/tutorials/tutorials_registry.mock'; export const registryMock = tutorialsRegistryMock.create(); jest.doMock('./services', () => ({ diff --git a/src/plugins/home/server/plugin.test.ts b/src/plugins/home/server/plugin.test.ts index e86a2d807109f..eec6501436bf4 100644 --- a/src/plugins/home/server/plugin.test.ts +++ b/src/plugins/home/server/plugin.test.ts @@ -18,13 +18,13 @@ */ import { registryMock } from './plugin.test.mocks'; -import { HomePlugin } from './plugin'; +import { HomeServerPlugin } from './plugin'; import { coreMock } from '../../../core/server/mocks'; import { CoreSetup } from '../../../core/server'; type MockedKeys = { [P in keyof T]: jest.Mocked }; -describe('HomePlugin', () => { +describe('HomeServerPlugin', () => { beforeEach(() => { registryMock.setup.mockClear(); registryMock.start.mockClear(); @@ -34,7 +34,7 @@ describe('HomePlugin', () => { const mockCoreSetup: MockedKeys = coreMock.createSetup(); test('wires up and returns registerTutorial and addScopedTutorialContextFactory', () => { - const setup = new HomePlugin().setup(mockCoreSetup); + const setup = new HomeServerPlugin().setup(mockCoreSetup); expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('registerTutorial'); expect(setup.tutorials).toHaveProperty('addScopedTutorialContextFactory'); @@ -43,7 +43,7 @@ describe('HomePlugin', () => { describe('start', () => { test('is defined', () => { - const start = new HomePlugin().start(); + const start = new HomeServerPlugin().start(); expect(start).toBeDefined(); expect(start).toHaveProperty('tutorials'); }); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index d5a3f235f8490..89dda8205ce02 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -19,7 +19,7 @@ import { CoreSetup, Plugin } from 'src/core/server'; import { TutorialsRegistry, TutorialsRegistrySetup, TutorialsRegistryStart } from './services'; -export class HomePlugin implements Plugin { +export class HomeServerPlugin implements Plugin { private readonly tutorialsRegistry = new TutorialsRegistry(); public setup(core: CoreSetup) { @@ -36,11 +36,11 @@ export class HomePlugin implements Plugin { } /** @public */ -export interface HomePluginSetup { +export interface HomeServerPluginSetup { tutorials: TutorialsRegistrySetup; } /** @public */ -export interface HomePluginStart { +export interface HomeServerPluginStart { tutorials: TutorialsRegistryStart; } diff --git a/src/plugins/home/server/services/index.ts b/src/plugins/home/server/services/index.ts index 5fe5cb0ba4760..9bfbe4079c6be 100644 --- a/src/plugins/home/server/services/index.ts +++ b/src/plugins/home/server/services/index.ts @@ -19,9 +19,17 @@ // provided to other plugins as APIs // should model the plugin lifecycle +export { TutorialsRegistry, TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials'; export { - TutorialsRegistry, - TutorialsRegistrySetup, - TutorialsRegistryStart, -} from './tutorials_registry'; -export * from '../lib/tutorials_registry_types'; + TutorialsCategory, + ParamTypes, + InstructionSetSchema, + ParamsSchema, + InstructionsSchema, + DashboardSchema, + ArtifactsSchema, + TutorialSchema, + TutorialProvider, + TutorialContextFactory, + ScopedTutorialContextFactory, +} from './tutorials'; diff --git a/src/plugins/home/server/services/tutorials/index.ts b/src/plugins/home/server/services/tutorials/index.ts new file mode 100644 index 0000000000000..d481a94516163 --- /dev/null +++ b/src/plugins/home/server/services/tutorials/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { + TutorialsRegistry, + TutorialsRegistrySetup, + TutorialsRegistryStart, +} from './tutorials_registry'; +export { + TutorialsCategory, + ParamTypes, + InstructionSetSchema, + ParamsSchema, + InstructionsSchema, + DashboardSchema, + ArtifactsSchema, + TutorialSchema, + TutorialProvider, + TutorialContextFactory, + ScopedTutorialContextFactory, +} from './lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts similarity index 100% rename from src/plugins/home/server/lib/tutorial_schema.ts rename to src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts diff --git a/src/plugins/home/server/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts similarity index 100% rename from src/plugins/home/server/lib/tutorials_registry_types.ts rename to src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts diff --git a/src/plugins/home/server/services/tutorials_registry.mock.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts similarity index 100% rename from src/plugins/home/server/services/tutorials_registry.mock.ts rename to src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts diff --git a/src/plugins/home/server/services/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts similarity index 95% rename from src/plugins/home/server/services/tutorials_registry.test.ts rename to src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index 04c26bab1f065..8144fef2d92e4 100644 --- a/src/plugins/home/server/services/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -18,16 +18,16 @@ */ import { TutorialsRegistry } from './tutorials_registry'; -import { coreMock } from '../../../../core/server/mocks'; -import { CoreSetup } from '../../../../core/server'; -import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { coreMock } from '../../../../../core/server/mocks'; +import { CoreSetup } from '../../../../../core/server'; +import { httpServerMock } from '../../../../../core/server/mocks'; import { TutorialProvider, TutorialSchema, TutorialsCategory, ScopedTutorialContextFactory, -} from '../lib/tutorials_registry_types'; +} from './lib/tutorials_registry_types'; const INVALID_TUTORIAL: TutorialSchema = { id: 'test', diff --git a/src/plugins/home/server/services/tutorials_registry.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.ts similarity index 96% rename from src/plugins/home/server/services/tutorials_registry.ts rename to src/plugins/home/server/services/tutorials/tutorials_registry.ts index 40692d8558656..be0302cbd8188 100644 --- a/src/plugins/home/server/services/tutorials_registry.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.ts @@ -23,8 +23,8 @@ import { TutorialProvider, TutorialContextFactory, ScopedTutorialContextFactory, -} from '../lib/tutorials_registry_types'; -import { tutorialSchema } from '../lib/tutorial_schema'; +} from './lib/tutorials_registry_types'; +import { tutorialSchema } from './lib/tutorial_schema'; export class TutorialsRegistry { private readonly tutorialProviders: TutorialProvider[] = []; // pre-register all the tutorials we know we want in here diff --git a/src/plugins/kibana_legacy/README.md b/src/plugins/kibana_legacy/README.md new file mode 100644 index 0000000000000..82bf3270589db --- /dev/null +++ b/src/plugins/kibana_legacy/README.md @@ -0,0 +1,6 @@ +# kibana-legacy + +This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + +Currently, the only service offered is the ability to register apps which are rendered in the legacy "kibana" plugin. + diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json new file mode 100644 index 0000000000000..26ee6db3ba06a --- /dev/null +++ b/src/plugins/kibana_legacy/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "kibana_legacy", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts new file mode 100644 index 0000000000000..4cb30be8917ac --- /dev/null +++ b/src/plugins/kibana_legacy/public/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { KibanaLegacyPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaLegacyPlugin(); +} + +export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts new file mode 100644 index 0000000000000..cb95088320d7b --- /dev/null +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { App } from 'kibana/public'; + +interface ForwardDefinition { + legacyAppId: string; + newAppId: string; + keepPrefix: boolean; +} + +export class KibanaLegacyPlugin { + private apps: App[] = []; + private forwards: ForwardDefinition[] = []; + + public setup() { + return { + /** + * @deprecated + * Register an app to be managed by the application service. + * This method works exactly as `core.application.register`. + * + * When an app is mounted, it is responsible for routing. The app + * won't be mounted again if the route changes within the prefix + * of the app (its id). It is fine to use whatever means for handling + * routing within the app. + * + * When switching to a URL outside of the current prefix, the app router + * shouldn't do anything because it doesn't own the routing anymore - + * the local application service takes over routing again, + * unmounts the current app and mounts the next app. + * + * @param app The app descriptor + */ + registerLegacyApp: (app: App) => { + this.apps.push(app); + }, + + /** + * @deprecated + * Forwards every URL starting with `legacyAppId` to the same URL starting + * with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to + * `/newApp/my/legacy/path?q=123`. + * + * When setting the `keepPrefix` option, the new app id is simply prepended. + * The example above would become `/newApp/legacy/my/legacy/path?q=123`. + * + * This method can be used to provide backwards compatibility for URLs when + * renaming or nesting plugins. For route changes after the prefix, please + * use the routing mechanism of your app. + * + * @param legacyAppId The name of the old app to forward URLs from + * @param newAppId The name of the new app that handles the URLs now + * @param options Whether the prefix of the old app is kept to nest the legacy + * path into the new path + */ + forwardApp: ( + legacyAppId: string, + newAppId: string, + options: { keepPrefix: boolean } = { keepPrefix: false } + ) => { + this.forwards.push({ legacyAppId, newAppId, ...options }); + }, + }; + } + + public start() { + return { + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getApps: () => this.apps, + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getForwards: () => this.forwards, + }; + } +} + +export type KibanaLegacySetup = ReturnType; +export type KibanaLegacyStart = ReturnType; diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json new file mode 100644 index 0000000000000..edebf8cb12239 --- /dev/null +++ b/src/plugins/status_page/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "status_page", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/status_page/public/index.ts b/src/plugins/status_page/public/index.ts new file mode 100644 index 0000000000000..db1f05cac076f --- /dev/null +++ b/src/plugins/status_page/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new StatusPagePlugin(); diff --git a/src/plugins/status_page/public/plugin.ts b/src/plugins/status_page/public/plugin.ts new file mode 100644 index 0000000000000..d072fd4a67c30 --- /dev/null +++ b/src/plugins/status_page/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; + +export class StatusPagePlugin implements Plugin { + public setup(core: CoreSetup) { + const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( + 'isStatusPageAnonymous' + ) as boolean; + + if (isStatusPageAnonymous) { + core.http.anonymousPaths.register('/status'); + } + } + + public start() {} + + public stop() {} +} + +export type StatusPagePluginSetup = ReturnType; +export type StatusPagePluginStart = ReturnType; diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts index 44eea308a31d9..601db10f6f8bb 100644 --- a/src/plugins/testbed/public/index.ts +++ b/src/plugins/testbed/public/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import { PluginInitializer } from 'kibana/public'; +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; -export const plugin: PluginInitializer = () => - new TestbedPlugin(); +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new TestbedPlugin(initializerContext); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts index bf51dbf0b8e78..8c70485d9ee8b 100644 --- a/src/plugins/testbed/public/plugin.ts +++ b/src/plugins/testbed/public/plugin.ts @@ -17,12 +17,20 @@ * under the License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; + +interface ConfigType { + uiProp: string; +} export class TestbedPlugin implements Plugin { - public setup(core: CoreSetup, deps: {}) { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = this.initializerContext.config.get(); + // eslint-disable-next-line no-console - console.log(`Testbed plugin set up`); + console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); return { foo: 'bar', }; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 4dd22d3dce1ef..07fda4eb98727 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -20,15 +20,28 @@ import { map, mergeMap } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; -import { CoreSetup, CoreStart, Logger, PluginInitializerContext, PluginName } from 'kibana/server'; +import { + CoreSetup, + CoreStart, + Logger, + PluginInitializerContext, + PluginConfigDescriptor, + PluginName, +} from 'kibana/server'; -export const config = { - schema: schema.object({ - secret: schema.string({ defaultValue: 'Not really a secret :/' }), - }), -}; +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Not really a secret :/' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; -type ConfigType = TypeOf; +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + uiProp: true, + }, + schema: configSchema, +}; class Plugin { private readonly log: Logger; diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md new file mode 100644 index 0000000000000..4502e1a6ceacf --- /dev/null +++ b/src/plugins/usage_collection/README.md @@ -0,0 +1,139 @@ +# Kibana Usage Collection Service + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + +1. Create a usage collector. +2. Register the usage collector. + +## Creating and Registering Usage Collector + +All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. + +### New Platform: + +1. Make sure `usageCollection` is in your optional Plugins: + +```json +// plugin/kibana.json +{ + "id": "...", + "optionalPlugins": ["usageCollection"] +} +``` + +2. Register Usage collector in the `setup` function: + +```ts +// server/plugin.ts +class Plugin { + setup(core, plugins) { + registerMyPluginUsageCollector(plugins.usageCollection); + } +} +``` + +3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` + +Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. + +Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. + +### Migrating to NP from Legacy Plugins: + +Pass `usageCollection` to the setup NP plugin setup function under plugins. Inside the `setup` function call the `registerCollector` like what you'd do in the NP example above. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + const plugins = { + usageCollection, + }; + plugin(initializerContext).setup(core, plugins); + } + }); +} +``` + +### Legacy Plugins: + +Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + registerMyPluginUsageCollector(usageCollection); + } + }); +} +``` + +## Update the telemetry payload and telemetry cluster field mappings + +There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. + +New fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. + +## Testing + +There are a few ways you can test that your usage collector is working properly. + +1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: + - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. + - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. + - The dev script in x-pack can be run on the command-line with: + ``` + cd x-pack + node scripts/api_debug.js telemetry --host=http://localhost:5601 + ``` + Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. + - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 +3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. +4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. + +## FAQ + +1. **How should I design my data model?** + Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. +2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** + Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/plugins/usage_collection/common/constants.ts b/src/plugins/usage_collection/common/constants.ts new file mode 100644 index 0000000000000..edd06b171a72c --- /dev/null +++ b/src/plugins/usage_collection/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const KIBANA_STATS_TYPE = 'kibana_stats'; diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json new file mode 100644 index 0000000000000..145cd89ff884d --- /dev/null +++ b/src/plugins/usage_collection/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "usageCollection", + "configPath": ["usageCollection"], + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/plugins/usage_collection/server/collector/__tests__/collector_set.js b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js new file mode 100644 index 0000000000000..a2e400b876ff7 --- /dev/null +++ b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js @@ -0,0 +1,188 @@ +/* + * 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 { noop } from 'lodash'; +import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { Collector } from '../collector'; +import { CollectorSet } from '../collector_set'; +import { UsageCollector } from '../usage_collector'; + +const mockLogger = () => ({ + debug: sinon.spy(), + warn: sinon.spy(), +}); + +describe('CollectorSet', () => { + describe('registers a collector set and runs lifecycle events', () => { + let init; + let fetch; + beforeEach(() => { + init = noop; + fetch = noop; + }); + + it('should throw an error if non-Collector type of object is registered', () => { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + const registerPojo = () => { + collectors.registerCollector({ + type: 'type_collector_test', + init, + fetch, + }); + }; + + expect(registerPojo).to.throwException(({ message }) => { + expect(message).to.be('CollectorSet can only have Collector instances registered'); + }); + }); + + it('should log debug status of fetching from the collector', async () => { + const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { + type: 'MY_TEST_COLLECTOR', + fetch: caller => caller() + })); + + const result = await collectors.bulkFetch(mockCallCluster); + const calls = logger.debug.getCalls(); + expect(calls.length).to.be(1); + expect(calls[0].args).to.eql([ + 'Fetching data from MY_TEST_COLLECTOR collector', + ]); + expect(result).to.eql([{ + type: 'MY_TEST_COLLECTOR', + result: { passTest: 1000 } + }]); + }); + + it('should gracefully handle a collector fetch method throwing an error', async () => { + const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { + type: 'MY_TEST_COLLECTOR', + fetch: () => new Promise((_resolve, reject) => reject()) + })); + + let result; + try { + result = await collectors.bulkFetch(mockCallCluster); + } catch (err) { + // Do nothing + } + // This must return an empty object instead of null/undefined + expect(result).to.eql([]); + }); + }); + + describe('toApiFieldNames', () => { + let collectorSet; + + beforeEach(() => { + const logger = mockLogger(); + collectorSet = new CollectorSet({ logger }); + }); + + it('should snake_case and convert field names to api standards', () => { + const apiData = { + os: { + load: { + '15m': 2.3525390625, + '1m': 2.22412109375, + '5m': 2.4462890625 + }, + memory: { + free_in_bytes: 458280960, + total_in_bytes: 17179869184, + used_in_bytes: 16721588224 + }, + uptime_in_millis: 137844000 + }, + daysOfTheWeek: [ + 'monday', + 'tuesday', + 'wednesday', + ] + }; + + const result = collectorSet.toApiFieldNames(apiData); + expect(result).to.eql({ + os: { + load: { '15m': 2.3525390625, '1m': 2.22412109375, '5m': 2.4462890625 }, + memory: { free_bytes: 458280960, total_bytes: 17179869184, used_bytes: 16721588224 }, + uptime_ms: 137844000, + }, + days_of_the_week: ['monday', 'tuesday', 'wednesday'], + }); + }); + + it('should correct object key fields nested in arrays', () => { + const apiData = { + daysOfTheWeek: [ + { + dayName: 'monday', + dayIndex: 1 + }, + { + dayName: 'tuesday', + dayIndex: 2 + }, + { + dayName: 'wednesday', + dayIndex: 3 + } + ] + }; + + const result = collectorSet.toApiFieldNames(apiData); + expect(result).to.eql({ + days_of_the_week: [ + { day_index: 1, day_name: 'monday' }, + { day_index: 2, day_name: 'tuesday' }, + { day_index: 3, day_name: 'wednesday' }, + ], + }); + }); + }); + + describe('isUsageCollector', () => { + const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} }; + + it('returns true only for UsageCollector instances', () => { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + const usageCollector = new UsageCollector(logger, collectorOptions); + const collector = new Collector(logger, collectorOptions); + const randomClass = new (class Random {}); + expect(collectors.isUsageCollector(usageCollector)).to.be(true); + expect(collectors.isUsageCollector(collector)).to.be(false); + expect(collectors.isUsageCollector(randomClass)).to.be(false); + expect(collectors.isUsageCollector({})).to.be(false); + expect(collectors.isUsageCollector(null)).to.be(false); + expect(collectors.isUsageCollector('')).to.be(false); + expect(collectors.isUsageCollector()).to.be(false); + }); + }); +}); + + diff --git a/src/legacy/server/usage/classes/collector.js b/src/plugins/usage_collection/server/collector/collector.js similarity index 93% rename from src/legacy/server/usage/classes/collector.js rename to src/plugins/usage_collection/server/collector/collector.js index 40b004f51e49a..ab723edf5b719 100644 --- a/src/legacy/server/usage/classes/collector.js +++ b/src/plugins/usage_collection/server/collector/collector.js @@ -17,18 +17,17 @@ * under the License. */ -import { getCollectorLogger } from '../lib'; export class Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { + constructor(logger, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } @@ -39,7 +38,7 @@ export class Collector { throw new Error('Collector must be instantiated with a options.fetch function property'); } - this.log = getCollectorLogger(server); + this.log = logger; Object.assign(this, options); // spread in other properties and mutate "this" diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts new file mode 100644 index 0000000000000..a87accc47535e --- /dev/null +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -0,0 +1,209 @@ +/* + * 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 { snakeCase } from 'lodash'; +import { Logger } from 'kibana/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +// @ts-ignore +import { Collector } from './collector'; +// @ts-ignore +import { UsageCollector } from './usage_collector'; + +interface CollectorSetConfig { + logger: Logger; + maximumWaitTimeForAllCollectorsInS: number; + collectors?: Collector[]; +} + +export class CollectorSet { + private _waitingForAllCollectorsTimestamp?: number; + private logger: Logger; + private readonly maximumWaitTimeForAllCollectorsInS: number; + private collectors: Collector[] = []; + constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) { + this.logger = logger; + this.collectors = collectors; + this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60; + } + + public makeStatsCollector = (options: any) => { + return new Collector(this.logger, options); + }; + public makeUsageCollector = (options: any) => { + return new UsageCollector(this.logger, options); + }; + + /* + * @param collector {Collector} collector object + */ + public registerCollector = (collector: Collector) => { + // check instanceof + if (!(collector instanceof Collector)) { + throw new Error('CollectorSet can only have Collector instances registered'); + } + + this.collectors.push(collector); + + if (collector.init) { + this.logger.debug(`Initializing ${collector.type} collector`); + collector.init(); + } + }; + + public getCollectorByType = (type: string) => { + return this.collectors.find(c => c.type === type); + }; + + public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => { + return x instanceof UsageCollector; + }; + + public areAllCollectorsReady = async (collectorSet = this) => { + if (!(collectorSet instanceof CollectorSet)) { + throw new Error( + `areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet + ); + } + + const collectorTypesNotReady: string[] = []; + let allReady = true; + for (const collector of collectorSet.collectors) { + if (!(await collector.isReady())) { + allReady = false; + collectorTypesNotReady.push(collector.type); + } + } + + if (!allReady && this.maximumWaitTimeForAllCollectorsInS >= 0) { + const nowTimestamp = +new Date(); + this._waitingForAllCollectorsTimestamp = + this._waitingForAllCollectorsTimestamp || nowTimestamp; + const timeWaitedInMS = nowTimestamp - this._waitingForAllCollectorsTimestamp; + const timeLeftInMS = this.maximumWaitTimeForAllCollectorsInS * 1000 - timeWaitedInMS; + if (timeLeftInMS <= 0) { + this.logger.debug( + `All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` + + `but we have waited the required ` + + `${this.maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.` + ); + return true; + } else { + this.logger.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); + } + } else { + this._waitingForAllCollectorsTimestamp = undefined; + } + + return allReady; + }; + + public bulkFetch = async ( + callCluster: CallCluster, + collectors: Collector[] = this.collectors + ) => { + const responses = []; + for (const collector of collectors) { + this.logger.debug(`Fetching data from ${collector.type} collector`); + try { + responses.push({ + type: collector.type, + result: await collector.fetchInternal(callCluster), + }); + } catch (err) { + this.logger.warn(err); + this.logger.warn(`Unable to fetch data from ${collector.type} collector`); + } + } + + return responses; + }; + + /* + * @return {new CollectorSet} + */ + public getFilteredCollectorSet = (filter: any) => { + const filtered = this.collectors.filter(filter); + return this.makeCollectorSetFromArray(filtered); + }; + + public bulkFetchUsage = async (callCluster: CallCluster) => { + const usageCollectors = this.getFilteredCollectorSet((c: any) => c instanceof UsageCollector); + return await this.bulkFetch(callCluster, usageCollectors.collectors); + }; + + // convert an array of fetched stats results into key/object + public toObject = (statsData: any) => { + if (!statsData) return {}; + return statsData.reduce((accumulatedStats: any, { type, result }: any) => { + return { + ...accumulatedStats, + [type]: result, + }; + }, {}); + }; + + // rename fields to use api conventions + public toApiFieldNames = (apiData: any): any => { + const getValueOrRecurse = (value: any) => { + if (value == null || typeof value !== 'object') { + return value; + } else { + return this.toApiFieldNames(value); // recurse + } + }; + + // handle array and return early, or return a reduced object + + if (Array.isArray(apiData)) { + return apiData.map(getValueOrRecurse); + } + + return Object.keys(apiData).reduce((accum, field) => { + const value = apiData[field]; + let newName = field; + newName = snakeCase(newName); + newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m + newName = newName.replace('_in_bytes', '_bytes'); + newName = newName.replace('_in_millis', '_ms'); + + return { + ...accum, + [newName]: getValueOrRecurse(value), + }; + }, {}); + }; + + // TODO: remove + public map = (mapFn: any) => { + return this.collectors.map(mapFn); + }; + + // TODO: remove + public some = (someFn: any) => { + return this.collectors.some(someFn); + }; + + private makeCollectorSetFromArray = (collectors: Collector[]) => { + return new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: this.maximumWaitTimeForAllCollectorsInS, + collectors, + }); + }; +} diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts new file mode 100644 index 0000000000000..962f61474c250 --- /dev/null +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { CollectorSet } from './collector_set'; +// @ts-ignore +export { Collector } from './collector'; +// @ts-ignore +export { UsageCollector } from './usage_collector'; diff --git a/src/legacy/server/usage/classes/usage_collector.js b/src/plugins/usage_collection/server/collector/usage_collector.js similarity index 88% rename from src/legacy/server/usage/classes/usage_collector.js rename to src/plugins/usage_collection/server/collector/usage_collector.js index 559deaef2ce15..1e2806ea15f3b 100644 --- a/src/legacy/server/usage/classes/usage_collector.js +++ b/src/plugins/usage_collection/server/collector/usage_collector.js @@ -17,20 +17,20 @@ * under the License. */ -import { KIBANA_STATS_TYPE } from '../../status/constants'; +import { KIBANA_STATS_TYPE } from '../../common/constants'; import { Collector } from './collector'; export class UsageCollector extends Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { - super(server, { type, init, fetch, formatForBulkUpload, ...options }); + constructor(logger, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { + super(logger, { type, init, fetch, formatForBulkUpload, ...options }); /* * Currently, for internal bulk uploading, usage stats are part of diff --git a/src/plugins/usage_collection/server/config.ts b/src/plugins/usage_collection/server/config.ts new file mode 100644 index 0000000000000..987db1f2b0ff3 --- /dev/null +++ b/src/plugins/usage_collection/server/config.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }), +}); diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts new file mode 100644 index 0000000000000..33a1a0adc6713 --- /dev/null +++ b/src/plugins/usage_collection/server/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { Plugin } from './plugin'; +import { ConfigSchema } from './config'; + +export { UsageCollectionSetup } from './plugin'; +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts new file mode 100644 index 0000000000000..e8bbc8e512a41 --- /dev/null +++ b/src/plugins/usage_collection/server/plugin.ts @@ -0,0 +1,55 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { TypeOf } from '@kbn/config-schema'; +import { ConfigSchema } from './config'; +import { PluginInitializerContext, Logger } from '../../../../src/core/server'; +import { CollectorSet } from './collector'; + +export type UsageCollectionSetup = CollectorSet; + +export class Plugin { + logger: Logger; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(): Promise { + const config = await this.initializerContext.config + .create>() + .pipe(first()) + .toPromise(); + + const collectorSet = new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, + }); + + return collectorSet; + } + + public start() { + this.logger.debug('Starting plugin'); + } + + public stop() { + this.logger.debug('Stopping plugin'); + } +} diff --git a/src/test_utils/public/stub_field_formats.ts b/src/test_utils/public/stub_field_formats.ts new file mode 100644 index 0000000000000..39c6fb2f6d10e --- /dev/null +++ b/src/test_utils/public/stub_field_formats.ts @@ -0,0 +1,64 @@ +/* + * 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 { UiSettingsClientContract } from 'kibana/public'; + +import { + FieldFormatRegisty, + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DurationFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, +} from '../../plugins/data/public/'; + +export const getFieldFormatsRegistry = (uiSettings: UiSettingsClientContract) => { + const fieldFormats = new FieldFormatRegisty(); + + fieldFormats.register([ + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DurationFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, + ]); + + fieldFormats.init(uiSettings); + + return fieldFormats; +}; diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index a6a0eb386d32e..b41ebe3e61861 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -28,9 +28,15 @@ import { formatHitProvider, flattenHitWrapper, } from 'ui/index_patterns'; -import { fieldFormats } from 'ui/registry/field_formats'; +import { + FIELD_FORMAT_IDS, +} from '../../plugins/data/public'; + +import { getFieldFormatsRegistry } from './stub_field_formats'; + +export default function StubIndexPattern(pattern, getConfig, timeField, fields, uiSettings) { + const registeredFieldFormats = getFieldFormatsRegistry(uiSettings); -export default function StubIndexPattern(pattern, getConfig, timeField, fields) { this.id = pattern; this.title = pattern; this.popularizeField = sinon.stub(); @@ -47,7 +53,7 @@ export default function StubIndexPattern(pattern, getConfig, timeField, fields) this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = flattenHitWrapper(this, this.metaFields); - this.formatHit = formatHitProvider(this, fieldFormats.getDefaultInstance('string')); + this.formatHit = formatHitProvider(this, registeredFieldFormats.getDefaultInstance(FIELD_FORMAT_IDS.STRING)); this.fieldsFetcher = { apiClient: { baseUrl: '' } }; this.formatField = this.formatHit.formatField; @@ -56,7 +62,7 @@ export default function StubIndexPattern(pattern, getConfig, timeField, fields) }; this.stubSetFieldFormat = function (fieldName, id, params) { - const FieldFormat = fieldFormats.getType(id); + const FieldFormat = registeredFieldFormats.getType(id); this.fieldFormatMap[fieldName] = new FieldFormat(params); this._reindexFields(); }; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 16947a97a3d14..25723677390bd 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -34,6 +34,19 @@ module.exports = function (grunt) { return 'Chrome'; } + function pickReporters() { + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + if (process.env.CI && process.env.DISABLE_JUNIT_REPORTER) { + return ['dots']; + } + + if (process.env.CI) { + return ['dots', 'junit']; + } + + return ['progress']; + } + const config = { options: { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -63,14 +76,13 @@ module.exports = function (grunt) { }, }, - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: process.env.CI ? ['dots', 'junit'] : ['progress'], + reporters: pickReporters(), junitReporter: { outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), useBrowserName: false, - nameFormatter: (browser, result) => [...result.suite, result.description].join(' '), - classNameFormatter: (browser, result) => { + nameFormatter: (_, result) => [...result.suite, result.description].join(' '), + classNameFormatter: (_, result) => { const rootSuite = result.suite[0] || result.description; return `Browser Unit Tests.${rootSuite.replace(/\./g, '·')}`; }, diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 7c3e597ae12d2..a9d066f3cd49f 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -19,8 +19,8 @@ module.exports = { kuery: { - src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg', - dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js', + src: 'src/plugins/data/common/es_query/kuery/ast/kuery.peg', + dest: 'src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js', options: { allowedStartRules: ['start', 'Literal'] } diff --git a/tasks/config/run.js b/tasks/config/run.js index ea5a4b01dc8a5..e4071c8b7d0ab 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -254,7 +254,7 @@ module.exports = function (grunt) { cmd: NODE, args: [ 'scripts/functional_tests', - '--config', 'test/interpreter_functional/config.js', + '--config', 'test/interpreter_functional/config.ts', '--bail', '--debug', '--kibana-install-dir', KIBANA_INSTALL_DIR, diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index e3f73ad4bcaf8..38ee5b7db39c4 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -19,9 +19,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; -const FROM_TIME = '2015-09-19 06:31:44.000'; -const TO_TIME = '2015-09-23 18:31:44.000'; - export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker']); const a11y = getService('a11y'); @@ -36,7 +33,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(FROM_TIME, TO_TIME); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); it('main view', async () => { diff --git a/test/api_integration/apis/home/sample_data.js b/test/api_integration/apis/home/sample_data.js index 7ca66e87a9325..042f490768af0 100644 --- a/test/api_integration/apis/home/sample_data.js +++ b/test/api_integration/apis/home/sample_data.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7; diff --git a/test/api_integration/apis/index_patterns/es_errors/errors.js b/test/api_integration/apis/index_patterns/es_errors/errors.js index 4e50b965211c2..77e024c9d20cc 100644 --- a/test/api_integration/apis/index_patterns/es_errors/errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/errors.js @@ -34,7 +34,7 @@ import { } from './lib'; export default function ({ getService }) { - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('index_patterns/* error handler', () => { diff --git a/test/api_integration/apis/kql_telemetry/kql_telemetry.js b/test/api_integration/apis/kql_telemetry/kql_telemetry.js index 87d06ee9458d6..25a68bb4bb2b6 100644 --- a/test/api_integration/apis/kql_telemetry/kql_telemetry.js +++ b/test/api_integration/apis/kql_telemetry/kql_telemetry.js @@ -24,7 +24,7 @@ import { get } from 'lodash'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); describe('telemetry API', () => { before(() => esArchiver.load('saved_objects/basic')); diff --git a/test/api_integration/apis/management/saved_objects/find.js b/test/api_integration/apis/management/saved_objects/find.js index 0e7cf08fa5c26..6bb3c0cebddbf 100644 --- a/test/api_integration/apis/management/saved_objects/find.js +++ b/test/api_integration/apis/management/saved_objects/find.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index e77e08d949f2b..3a520d369120d 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const BULK_REQUESTS = [ diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js index 8c5b589a12a2e..52733aa70200b 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.js +++ b/test/api_integration/apis/saved_objects/bulk_get.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const BULK_REQUESTS = [ diff --git a/test/api_integration/apis/saved_objects/bulk_update.js b/test/api_integration/apis/saved_objects/bulk_update.js index 4bdf257ceef02..b38934aecd0fb 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.js +++ b/test/api_integration/apis/saved_objects/bulk_update.js @@ -22,7 +22,7 @@ import _ from 'lodash'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js index eefd9e8ab1a95..363aa9d30c08d 100644 --- a/test/api_integration/apis/saved_objects/create.js +++ b/test/api_integration/apis/saved_objects/create.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('create', () => { diff --git a/test/api_integration/apis/saved_objects/delete.js b/test/api_integration/apis/saved_objects/delete.js index a9037bf697406..8aa76e99312b0 100644 --- a/test/api_integration/apis/saved_objects/delete.js +++ b/test/api_integration/apis/saved_objects/delete.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('delete', () => { diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index 9ab7a09309952..59c9afe24b802 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('export', () => { diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 7b2b15d298ce0..e1bec19cf8e6a 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('find', () => { diff --git a/test/api_integration/apis/saved_objects/get.js b/test/api_integration/apis/saved_objects/get.js index fb4b6f91b8bb8..9034bc5d84a72 100644 --- a/test/api_integration/apis/saved_objects/get.js +++ b/test/api_integration/apis/saved_objects/get.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('get', () => { diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index 5c492951ec938..c4b258d47e24b 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -31,7 +31,7 @@ import { SavedObjectsSerializer } from '../../../../src/core/server/saved_object import { SavedObjectsSchema } from '../../../../src/core/server/saved_objects/schema'; export default ({ getService }) => { - const es = getService('es'); + const es = getService('legacyEs'); const callCluster = (path, ...args) => _.get(es, path).call(es, ...args); describe('Kibana index migration', () => { diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js index e6ad6b2b781da..c73ed0ecc6424 100644 --- a/test/api_integration/apis/saved_objects/update.js +++ b/test/api_integration/apis/saved_objects/update.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('update', () => { diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index f0c86f2904638..51959bf5f7fda 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -22,7 +22,7 @@ import { ReportManager, METRIC_TYPE } from '@kbn/analytics'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const createStatsMetric = (eventName) => ({ key: ReportManager.createMetricKey({ appName: 'myApp', type: METRIC_TYPE.CLICK, eventName }), diff --git a/test/api_integration/services/index.ts b/test/api_integration/services/index.ts index 573450cae534a..782ea271869ba 100644 --- a/test/api_integration/services/index.ts +++ b/test/api_integration/services/index.ts @@ -23,10 +23,7 @@ import { services as commonServices } from '../../common/services'; import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; export const services = { - es: commonServices.es, - esArchiver: commonServices.esArchiver, - retry: commonServices.retry, + ...commonServices, supertest: KibanaSupertestProvider, esSupertest: ElasticsearchSupertestProvider, - randomness: commonServices.randomness, }; diff --git a/test/common/services/elasticsearch.ts b/test/common/services/elasticsearch.ts new file mode 100644 index 0000000000000..63c4bfeeb4ce7 --- /dev/null +++ b/test/common/services/elasticsearch.ts @@ -0,0 +1,33 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import { Client } from '@elastic/elasticsearch'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function ElasticsearchProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + + return new Client({ + nodes: [formatUrl(config.get('servers.elasticsearch'))], + requestTimeout: config.get('timeouts.esRequestTimeout'), + }); +} diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts index e72bb49a76c0d..cfe0610414b4f 100644 --- a/test/common/services/es_archiver.ts +++ b/test/common/services/es_archiver.ts @@ -26,7 +26,7 @@ import * as KibanaServer from './kibana_server'; export function EsArchiverProvider({ getService, hasService }: FtrProviderContext): EsArchiver { const config = getService('config'); - const client = getService('es'); + const client = getService('legacyEs'); const log = getService('log'); if (!config.get('esArchiver')) { diff --git a/test/common/services/index.ts b/test/common/services/index.ts index 225aacc1c9895..3454964f35e07 100644 --- a/test/common/services/index.ts +++ b/test/common/services/index.ts @@ -18,13 +18,15 @@ */ import { LegacyEsProvider } from './legacy_es'; +import { ElasticsearchProvider } from './elasticsearch'; import { EsArchiverProvider } from './es_archiver'; import { KibanaServerProvider } from './kibana_server'; import { RetryProvider } from './retry'; import { RandomnessProvider } from './randomness'; export const services = { - es: LegacyEsProvider, + legacyEs: LegacyEsProvider, + es: ElasticsearchProvider, esArchiver: EsArchiverProvider, kibanaServer: KibanaServerProvider, retry: RetryProvider, diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 147d0e74d1c98..cb328dfa98e3e 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -19,8 +19,6 @@ import expect from '@kbn/expect'; -const TEST_DISCOVER_START_TIME = '2015-09-19 06:31:44.000'; -const TEST_DISCOVER_END_TIME = '2015-09-23 18:31:44.000'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [['extension', 'jpg'], ['geo.src', 'IN']]; @@ -34,7 +32,7 @@ export default function ({ getService, getPageObjects }) { this.tags('smoke'); before(async function () { await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(TEST_DISCOVER_START_TIME, TEST_DISCOVER_END_TIME); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await Promise.all(TEST_COLUMN_NAMES.map((columnName) => ( PageObjects.discover.clickFieldListItemAdd(columnName) ))); diff --git a/test/functional/apps/dashboard/dashboard_time.js b/test/functional/apps/dashboard/dashboard_time.js index 917157e54eee0..39ce43e19d751 100644 --- a/test/functional/apps/dashboard/dashboard_time.js +++ b/test/functional/apps/dashboard/dashboard_time.js @@ -21,9 +21,6 @@ import expect from '@kbn/expect'; const dashboardName = 'Dashboard Test Time'; -const fromTime = '2015-09-19 06:31:44.000'; -const toTime = '2015-09-23 18:31:44.000'; - export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['dashboard', 'header', 'timePicker']); const browser = getService('browser'); @@ -46,31 +43,31 @@ export default function ({ getPageObjects, getService }) { }); it('Does not set the time picker on open', async () => { - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.dashboard.loadSavedDashboard(dashboardName); const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.equal('Sep 19, 2015 @ 06:31:44.000'); - expect(time.end).to.equal('Sep 23, 2015 @ 18:31:44.000'); + expect(time.start).to.equal(PageObjects.timePicker.defaultStartTime); + expect(time.end).to.equal(PageObjects.timePicker.defaultEndTime); }); }); describe('dashboard with stored timed', function () { it('is saved with time', async function () { await PageObjects.dashboard.switchToEditMode(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); }); it('sets time on open', async function () { - await PageObjects.timePicker.setAbsoluteRange('2019-01-01 00:00:00.000', '2019-01-02 00:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Jan 1, 2019 @ 00:00:00.000', 'Jan 2, 2019 @ 00:00:00.000'); await PageObjects.dashboard.loadSavedDashboard(dashboardName); const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.equal('Sep 19, 2015 @ 06:31:44.000'); - expect(time.end).to.equal('Sep 23, 2015 @ 18:31:44.000'); + expect(time.start).to.equal(PageObjects.timePicker.defaultStartTime); + expect(time.end).to.equal(PageObjects.timePicker.defaultEndTime); }); // If time is stored with a dashboard, it's supposed to override the current time settings when opened. @@ -99,7 +96,7 @@ export default function ({ getPageObjects, getService }) { it('preserved during navigation', async function () { await PageObjects.dashboard.loadSavedDashboard(dashboardName); - await PageObjects.timePicker.setAbsoluteRange('2019-01-01 00:00:00.000', '2019-01-02 00:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Jan 1, 2019 @ 00:00:00.000', 'Jan 2, 2019 @ 00:00:00.000'); await PageObjects.header.clickVisualize(); await PageObjects.header.clickDashboard(); diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js index f09f3fc1de2ad..ab1d1ff74168a 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.js +++ b/test/functional/apps/dashboard/dashboard_time_picker.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const dashboardVisualizations = getService('dashboardVisualizations'); const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'timePicker']); const browser = getService('browser'); + const kibanaServer = getService('kibanaServer'); describe('dashboard time picker', function describeIndexTests() { before(async function () { @@ -33,6 +34,11 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.preserveCrossAppState(); }); + after(async () => { + await kibanaServer.uiSettings.replace({}); + await browser.refresh(); + }); + it('Visualization updated when time picker changes', async () => { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.addVisualizations([PIE_CHART_VIS_NAME]); @@ -49,7 +55,7 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.docTableFieldCount(150); // Set to time range with no data - await PageObjects.timePicker.setAbsoluteRange('2000-01-01 00:00:00.000', '2000-01-01 01:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Jan 1, 2000 @ 00:00:00.000', 'Jan 1, 2000 @ 01:00:00.000'); await dashboardExpect.docTableFieldCount(0); }); @@ -72,5 +78,16 @@ export default function ({ getService, getPageObjects }) { expect(time.end).to.be('Nov 17, 2015 @ 18:01:36.621'); expect(refresh.interval).to.be('2'); }); + + it('Timepicker respects dateFormat from UI settings', async () => { + await kibanaServer.uiSettings.replace({ 'dateFormat': 'YYYY-MM-DD HH:mm:ss.SSS', }); + await browser.refresh(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.addVisualizations([PIE_CHART_VIS_NAME]); + // Same date range as `setTimepickerInHistoricalDataRange` + await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-23 18:31:44.000'); + await pieChart.expectPieSliceCount(10); + }); }); } diff --git a/test/functional/apps/dashboard/embed_mode.js b/test/functional/apps/dashboard/embed_mode.js index 7122d9ff8ca25..9eb5b2c9352d8 100644 --- a/test/functional/apps/dashboard/embed_mode.js +++ b/test/functional/apps/dashboard/embed_mode.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['dashboard', 'common']); const browser = getService('browser'); + const globalNav = getService('globalNav'); describe('embed mode', () => { before(async () => { @@ -38,8 +39,8 @@ export default function ({ getService, getPageObjects }) { }); it('hides the chrome', async () => { - const isChromeVisible = await PageObjects.common.isChromeVisible(); - expect(isChromeVisible).to.be(true); + const globalNavShown = await globalNav.exists(); + expect(globalNavShown).to.be(true); const currentUrl = await browser.getCurrentUrl(); const newUrl = currentUrl + '&embed=true'; @@ -48,8 +49,8 @@ export default function ({ getService, getPageObjects }) { await browser.get(newUrl.toString(), useTimeStamp); await retry.try(async () => { - const isChromeHidden = await PageObjects.common.isChromeHidden(); - expect(isChromeHidden).to.be(true); + const globalNavHidden = !(await globalNav.exists()); + expect(globalNavHidden).to.be(true); }); }); diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index d90de4204bc76..bdbfd41b437eb 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -102,8 +102,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); - const fromTime = '2018-01-01 00:00:00.000'; - const toTime = '2018-04-13 00:00:00.000'; + const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; + const toTime = 'Apr 13, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); @@ -145,8 +145,8 @@ export default function ({ getService, getPageObjects }) { it('data rendered correctly when dashboard is opened from listing page', async () => { // Change the time to make sure that it's updated when re-opened from the listing page. - const fromTime = '2018-05-10 00:00:00.000'; - const toTime = '2018-05-11 00:00:00.000'; + const fromTime = 'May 10, 2018 @ 00:00:00.000'; + const toTime = 'May 11, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.dashboard.loadSavedDashboard('embeddable rendering test'); await PageObjects.dashboard.waitForRenderComplete(); @@ -162,16 +162,16 @@ export default function ({ getService, getPageObjects }) { }); it('panels are updated when time changes outside of data', async () => { - const fromTime = '2018-05-11 00:00:00.000'; - const toTime = '2018-05-12 00:00:00.000'; + const fromTime = 'May 11, 2018 @ 00:00:00.000'; + const toTime = 'May 12, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.dashboard.waitForRenderComplete(); await expectNoDataRenders(); }); it('panels are updated when time changes inside of data', async () => { - const fromTime = '2018-01-01 00:00:00.000'; - const toTime = '2018-04-13 00:00:00.000'; + const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; + const toTime = 'Apr 13, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.dashboard.waitForRenderComplete(); await expectAllDataRenders(); diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index 19516e5092c1c..30031882ae09e 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -51,8 +51,8 @@ export default function ({ getService, getPageObjects }) { it('Exported dashboard adjusts EST time to UTC', async () => { const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('2018-04-10 03:00:00.000'); - expect(time.end).to.be('2018-04-10 04:00:00.000'); + expect(time.start).to.be('Apr 10, 2018 @ 03:00:00.000'); + expect(time.end).to.be('Apr 10, 2018 @ 04:00:00.000'); await pieChart.expectPieSliceCount(4); }); @@ -63,8 +63,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.loadSavedDashboard('time zone test'); const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('2018-04-09 22:00:00.000'); - expect(time.end).to.be('2018-04-09 23:00:00.000'); + expect(time.start).to.be('Apr 9, 2018 @ 22:00:00.000'); + expect(time.end).to.be('Apr 9, 2018 @ 23:00:00.000'); await pieChart.expectPieSliceCount(4); }); }); diff --git a/test/functional/apps/dashboard/view_edit.js b/test/functional/apps/dashboard/view_edit.js index 958a889271b61..74ddb476064c5 100644 --- a/test/functional/apps/dashboard/view_edit.js +++ b/test/functional/apps/dashboard/view_edit.js @@ -75,7 +75,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); await PageObjects.dashboard.switchToEditMode(); - await PageObjects.timePicker.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); + await PageObjects.timePicker.setAbsoluteRange('Sep 19, 2013 @ 06:31:44.000', 'Sep 19, 2013 @ 06:31:44.000'); await PageObjects.dashboard.clickCancelOutOfEditMode(); // confirm lose changes @@ -161,10 +161,10 @@ export default function ({ getService, getPageObjects }) { describe('and preserves edits on cancel', function () { it('when time changed is stored with dashboard', async function () { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); - await PageObjects.timePicker.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); + await PageObjects.timePicker.setAbsoluteRange('Sep 19, 2013 @ 06:31:44.000', 'Sep 19, 2013 @ 06:31:44.000'); await PageObjects.dashboard.saveDashboard(dashboardName, true); await PageObjects.dashboard.switchToEditMode(); - await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-19 06:31:44.000'); + await PageObjects.timePicker.setAbsoluteRange('Sep 19, 2015 @ 06:31:44.000', 'Sep 19, 2015 @ 06:31:44.000'); await PageObjects.dashboard.clickCancelOutOfEditMode(); await PageObjects.common.clickCancelOnModal(); @@ -186,7 +186,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.setTimepickerInDataRange(); await PageObjects.dashboard.saveDashboard(dashboardName, true); await PageObjects.dashboard.switchToEditMode(); - await PageObjects.timePicker.setAbsoluteRange('2013-09-19 06:31:44.000', '2013-09-19 06:31:44.000'); + await PageObjects.timePicker.setAbsoluteRange('Sep 19, 2013 @ 06:31:44.000', 'Sep 19, 2013 @ 06:31:44.000'); const newTime = await PageObjects.timePicker.getTimeConfig(); await PageObjects.dashboard.clickCancelOutOfEditMode(); @@ -208,7 +208,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: false }); await PageObjects.dashboard.switchToEditMode(); - await PageObjects.timePicker.setAbsoluteRange('2014-10-19 06:31:44.000', '2014-12-19 06:31:44.000'); + await PageObjects.timePicker.setAbsoluteRange('Oct 19, 2014 @ 06:31:44.000', 'Dec 19, 2014 @ 06:31:44.000'); await PageObjects.dashboard.clickCancelOutOfEditMode(); await PageObjects.common.expectConfirmModalOpenState(false); diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 6876c8eb6daa4..d9eb40c16c2d4 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -23,8 +23,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); - const fromTime = '2019-09-22 20:31:44.000'; - const toTime = '2019-09-23 03:31:44.000'; + const fromTime = 'Sep 22, 2019 @ 20:31:44.000'; + const toTime = 'Sep 23, 2019 @ 03:31:44.000'; describe('date_nanos', function () { @@ -41,8 +41,8 @@ export default function ({ getService, getPageObjects }) { it('should show a timestamp with nanoseconds in the first result row', async function () { const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.be('Sep 22, 2019 @ 20:31:44.000'); - expect(time.end).to.be('Sep 23, 2019 @ 03:31:44.000'); + expect(time.start).to.be(fromTime); + expect(time.end).to.be(toTime); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData.startsWith('Sep 22, 2019 @ 23:50:13.253123345')).to.be.ok(); }); diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js index 8c9a7eb4d5f13..c77ea3b2915e1 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.js +++ b/test/functional/apps/discover/_date_nanos_mixed.js @@ -23,8 +23,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); - const fromTime = '2019-01-01 00:00:00.000'; - const toTime = '2019-01-01 23:59:59.999'; + const fromTime = 'Jan 1, 2019 @ 00:00:00.000'; + const toTime = 'Jan 1, 2019 @ 23:59:59.999'; describe('date_nanos_mixed', function () { diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 9d3f95e28942a..94b2941ecd3d1 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -33,8 +33,6 @@ export default function ({ getService, getPageObjects }) { }; describe('discover test', function describeIndexTests() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; before(async function () { log.debug('load kibana index with default index pattern'); @@ -45,7 +43,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('query', function () { @@ -54,8 +52,8 @@ export default function ({ getService, getPageObjects }) { it('should show correct time range string by timepicker', async function () { const time = await PageObjects.timePicker.getTimeConfig(); - expect(time.start).to.be('Sep 19, 2015 @ 06:31:44.000'); - expect(time.end).to.be('Sep 23, 2015 @ 18:31:44.000'); + expect(time.start).to.be(PageObjects.timePicker.defaultStartTime); + expect(time.end).to.be(PageObjects.timePicker.defaultEndTime); const rowData = await PageObjects.discover.getDocTableIndex(1); log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)'); expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok(); @@ -88,12 +86,12 @@ export default function ({ getService, getPageObjects }) { it('should show correct time range string in chart', async function () { const actualTimeString = await PageObjects.discover.getChartTimespan(); - const expectedTimeString = `Sep 19, 2015 @ 06:31:44.000 - Sep 23, 2015 @ 18:31:44.000`; + const expectedTimeString = `${PageObjects.timePicker.defaultStartTime} - ${PageObjects.timePicker.defaultEndTime}`; expect(actualTimeString).to.be(expectedTimeString); }); it('should modify the time range when a bar is clicked', async function () { - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.clickHistogramBar(); const time = await PageObjects.timePicker.getTimeConfig(); expect(time.start).to.be('Sep 21, 2015 @ 09:00:00.000'); @@ -103,7 +101,7 @@ export default function ({ getService, getPageObjects }) { }); it('should modify the time range when the histogram is brushed', async function () { - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.brushHistogram(); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); @@ -113,7 +111,7 @@ export default function ({ getService, getPageObjects }) { }); it('should show correct initial chart interval of Auto', async function () { - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); const actualInterval = await PageObjects.discover.getChartInterval(); @@ -135,8 +133,8 @@ export default function ({ getService, getPageObjects }) { }); describe('query #2, which has an empty time range', () => { - const fromTime = '1999-06-11 09:22:11.000'; - const toTime = '1999-06-12 11:21:04.000'; + const fromTime = 'Jun 11, 1999 @ 09:22:11.000'; + const toTime = 'Jun 12, 1999 @ 11:21:04.000'; before(async () => { log.debug('setAbsoluteRangeForAnotherQuery'); @@ -159,7 +157,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { log.debug('setAbsoluteRangeForAnotherQuery'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); }); @@ -213,7 +211,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); await browser.refresh(); await PageObjects.header.awaitKibanaChrome(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('check that the newest doc timestamp is now -7 hours from the UTC time in the first test'); const rowData = await PageObjects.discover.getDocTableIndex(1); diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index e9afa7c9b4c19..dc9f6c3517464 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -19,8 +19,6 @@ import expect from '@kbn/expect'; -const TEST_DOC_START_TIME = '2015-09-19 06:31:44.000'; -const TEST_DOC_END_TIME = '2015-09-23 18:31:44.000'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [['extension', 'jpg'], ['geo.src', 'IN']]; @@ -35,7 +33,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(TEST_DOC_START_TIME, TEST_DOC_END_TIME); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await Promise.all(TEST_COLUMN_NAMES.map((columnName) => ( PageObjects.discover.clickFieldListItemAdd(columnName) ))); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index 828975445e1ef..38d9a89eecbf3 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -30,8 +30,6 @@ export default function ({ getService, getPageObjects }) { describe('discover tab', function describeIndexTests() { this.tags('smoke'); before(async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); @@ -41,7 +39,7 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('field data', function () { diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js index 6a2144e829920..bd04cd6d1bd64 100644 --- a/test/functional/apps/discover/_inspector.js +++ b/test/functional/apps/discover/_inspector.js @@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }) { }); it('should display request stats with results', async () => { - await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-23 18:31:44.000'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await inspector.open(); const requestStats = await inspector.getTableData(); diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 54b026135025c..8fbc40f86e8dc 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -34,8 +34,6 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); describe('saved queries saved objects', function describeIndexTests() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; before(async function () { log.debug('load kibana index with default index pattern'); @@ -46,7 +44,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('saved query management component functionality', function () { @@ -55,8 +53,8 @@ export default function ({ getService, getPageObjects }) { log.debug('set up a query with filters to save'); await queryBar.setQuery('response:200'); await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); - const fromTime = '2015-09-20 08:00:00.000'; - const toTime = '2015-09-21 08:00:00.000'; + const fromTime = 'Sep 20, 2015 @ 08:00:00.000'; + const toTime = 'Sep 21, 2015 @ 08:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); @@ -79,15 +77,13 @@ export default function ({ getService, getPageObjects }) { }); it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); await savedQueryManagementComponent.loadSavedQuery('OkResponse'); const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - expect(timePickerValues.start).to.not.eql(fromTime); - expect(timePickerValues.end).to.not.eql(toTime); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); }); it('allows saving changes to a currently loaded query via the saved query management component', async () => { diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 0b2b4f14f126d..8381d9bc7caea 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -38,9 +38,6 @@ export default function ({ getService, getPageObjects }) { baseUrl = baseUrl.replace(':80', '').replace(':443', ''); log.debug('New baseUrl = ' + baseUrl); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', @@ -57,7 +54,7 @@ export default function ({ getService, getPageObjects }) { log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); //After hiding the time picker, we need to wait for //the refresh button to hide before clicking the share button diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js index 26a47b0a12422..4cf8fd44da239 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.js @@ -27,8 +27,6 @@ export default function ({ getService, getPageObjects }) { describe('discover sidebar', function describeIndexTests() { before(async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ @@ -44,7 +42,7 @@ export default function ({ getService, getPageObjects }) { log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('field filtering', function () { diff --git a/test/functional/apps/discover/_source_filters.js b/test/functional/apps/discover/_source_filters.js index 14ecde383fd44..06c81acbd5d43 100644 --- a/test/functional/apps/discover/_source_filters.js +++ b/test/functional/apps/discover/_source_filters.js @@ -27,8 +27,6 @@ export default function ({ getService, getPageObjects }) { describe('source filters', function describeIndexTests() { before(async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ @@ -44,7 +42,7 @@ export default function ({ getService, getPageObjects }) { log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); //After hiding the time picker, we need to wait for //the refresh button to hide before clicking the share button diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index c60788b64b2a7..58e0793b2d547 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -28,8 +28,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; const getHost = () => { if (process.env.TEST_KIBANA_HOSTNAME) { @@ -77,7 +75,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { // Navigate to discover app await appsMenu.clickLink('Discover'); const discoverUrl = await browser.getCurrentUrl(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); // Navigate to dashboard app diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.js index 954c2dd110585..44cb7c36a7f28 100644 --- a/test/functional/apps/home/_sample_data.js +++ b/test/functional/apps/home/_sample_data.js @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; +import moment from 'moment'; export default function ({ getService, getPageObjects }) { const retry = getService('retry'); @@ -85,10 +86,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.home.launchSampleDataSet('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const today = new Date(); - const todayYearMonthDay = today.toISOString().substring(0, 10); - const fromTime = `${todayYearMonthDay} 00:00:00.000`; - const toTime = `${todayYearMonthDay} 23:59:59.999`; + const todayYearMonthDay = moment().format('MMM D, YYYY'); + const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; + const toTime = `${todayYearMonthDay} @ 23:59:59.999`; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(18); @@ -119,10 +119,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.home.launchSampleDataSet('logs'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const today = new Date(); - const todayYearMonthDay = today.toISOString().substring(0, 10); - const fromTime = `${todayYearMonthDay} 00:00:00.000`; - const toTime = `${todayYearMonthDay} 23:59:59.999`; + const todayYearMonthDay = moment().format('MMM D, YYYY'); + const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; + const toTime = `${todayYearMonthDay} @ 23:59:59.999`; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(11); @@ -132,10 +131,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.home.launchSampleDataSet('ecommerce'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const today = new Date(); - const todayYearMonthDay = today.toISOString().substring(0, 10); - const fromTime = `${todayYearMonthDay} 00:00:00.000`; - const toTime = `${todayYearMonthDay} 23:59:59.999`; + const todayYearMonthDay = moment().format('MMM D, YYYY'); + const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; + const toTime = `${todayYearMonthDay} @ 23:59:59.999`; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(12); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 1556897d69387..99941d37df9e7 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); @@ -72,8 +72,8 @@ export default function ({ getService, getPageObjects }) { it('should be able to discover and verify no of hits for alias2', async function () { const expectedHitCount = '5'; - const fromTime = '2016-11-12 05:00:00.000'; - const toTime = '2016-11-19 05:00:00.000'; + const fromTime = 'Nov 12, 2016 @ 05:00:00.000'; + const toTime = 'Nov 19, 2016 @ 05:00:00.000'; await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.selectIndexPattern('alias2'); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index 217e6d4c1a8d3..e6c0825136ba1 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -32,7 +32,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const scriptedFiledName = 'versionConflictScript'; const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'header']); diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index 9fb302cdba00a..e5ae709b34d6d 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -55,7 +55,7 @@ export default function ({ getService, getPageObjects }) { it('when false, dashboard state is unhashed', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-23 18:31:44.000'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); const currentUrl = await browser.getCurrentUrl(); const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); const globalState = urlPieces[2]; @@ -78,7 +78,7 @@ export default function ({ getService, getPageObjects }) { it('when true, dashboard state is hashed', async function () { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-23 18:31:44.000'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); const currentUrl = await browser.getCurrentUrl(); const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); const globalState = urlPieces[2]; diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 94e38402deebe..7a21e5171595f 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -124,8 +124,8 @@ export default function ({ getService, getPageObjects }) { }); it('should see scripted field value in Discover', async function () { - const fromTime = '2015-09-17 06:31:44.000'; - const toTime = '2015-09-18 18:31:44.000'; + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -186,8 +186,8 @@ export default function ({ getService, getPageObjects }) { }); it('should see scripted field value in Discover', async function () { - const fromTime = '2015-09-17 06:31:44.000'; - const toTime = '2015-09-18 18:31:44.000'; + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -246,8 +246,8 @@ export default function ({ getService, getPageObjects }) { }); it('should see scripted field value in Discover', async function () { - const fromTime = '2015-09-17 06:31:44.000'; - const toTime = '2015-09-18 18:31:44.000'; + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -307,8 +307,8 @@ export default function ({ getService, getPageObjects }) { }); it('should see scripted field value in Discover', async function () { - const fromTime = '2015-09-17 19:22:00.000'; - const toTime = '2015-09-18 07:00:00.000'; + const fromTime = 'Sep 17, 2015 @ 19:22:00.000'; + const toTime = 'Sep 18, 2015 @ 07:00:00.000'; await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -326,7 +326,7 @@ export default function ({ getService, getPageObjects }) { it('should filter by scripted field value in Discover', async function () { await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); - await log.debug('filter by "2015-09-17 23:00" in the expanded scripted field list'); + await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list'); await PageObjects.discover.clickFieldListPlusFilter(scriptedPainlessFieldName2, '2015-09-17 23:00'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js index 7cc0740823f3c..55297c6810497 100644 --- a/test/functional/apps/timelion/_expression_typeahead.js +++ b/test/functional/apps/timelion/_expression_typeahead.js @@ -24,11 +24,9 @@ export default function ({ getPageObjects }) { describe('expression typeahead', () => { before(async () => { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; await PageObjects.timelion.initTests(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); it('should display function suggestions filtered by function name', async () => { diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 963b7f25bc1c9..c6b33886a2456 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -30,8 +30,6 @@ export default function ({ getService, getPageObjects }) { const vizName1 = 'Visualization AreaChart Name Test'; const initAreaChart = async () => { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); @@ -39,7 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickAreaChart(); log.debug('clickNewSearch'); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Click X-axis'); await PageObjects.visualize.clickBucket('X-axis'); log.debug('Click Date Histogram'); @@ -327,8 +325,8 @@ export default function ({ getService, getPageObjects }) { }); describe('date histogram with long time range', () => { // that dataset spans from Oct 26, 2013 @ 06:10:17.855 to Apr 18, 2019 @ 11:38:12.790 - const fromTime = '2013-01-01 00:00:00.000'; - const toTime = '2020-01-01 00:00:00.000'; + const fromTime = 'Jan 1, 2013 @ 00:00:00.000'; + const toTime = 'Jan 1, 2020 @ 00:00:00.000'; it('should render a yearly area with 12 svg paths', async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index b540c1e949fbc..51c0984c89fed 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -26,9 +26,6 @@ export default function ({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - describe('data table', function indexPatternCreation() { const vizName1 = 'Visualization DataTable'; @@ -39,7 +36,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickDataTable(); log.debug('clickNewSearch'); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split rows'); await PageObjects.visualize.clickBucket('Split rows'); log.debug('Aggregation = Histogram'); @@ -112,7 +109,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Range'); await PageObjects.visualize.selectField('bytes'); @@ -157,7 +154,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Metric', 'metrics'); await PageObjects.visualize.selectAggregation('Average Bucket', 'metrics'); await PageObjects.visualize.selectAggregation('Terms', 'metrics', 'buckets'); @@ -172,7 +169,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Date Histogram'); await PageObjects.visualize.selectField('@timestamp'); @@ -191,7 +188,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Date Histogram'); await PageObjects.visualize.selectField('@timestamp'); @@ -227,7 +224,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickMetricEditor(); await PageObjects.visualize.selectAggregation('Top Hit', 'metrics'); await PageObjects.visualize.selectField('agent.raw', 'metrics'); @@ -241,7 +238,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Range'); await PageObjects.visualize.selectField('bytes'); @@ -259,7 +256,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Terms'); await PageObjects.visualize.selectField('extension.raw'); @@ -297,7 +294,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Terms'); await PageObjects.visualize.selectField('extension.raw'); @@ -392,7 +389,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split table'); await PageObjects.visualize.selectAggregation('Terms'); await PageObjects.visualize.selectField('extension.raw'); diff --git a/test/functional/apps/visualize/_embedding_chart.js b/test/functional/apps/visualize/_embedding_chart.js index 12bc53ec26507..fcf5e54954347 100644 --- a/test/functional/apps/visualize/_embedding_chart.js +++ b/test/functional/apps/visualize/_embedding_chart.js @@ -26,9 +26,6 @@ export default function ({ getService, getPageObjects }) { const embedding = getService('embedding'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'timePicker']); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - describe('embedding', () => { describe('a data table', () => { @@ -36,7 +33,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.clickBucket('Split rows'); await PageObjects.visualize.selectAggregation('Date Histogram'); await PageObjects.visualize.selectField('@timestamp'); diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 0d0383ba91aa7..2125717247e9e 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -29,8 +29,6 @@ export default function ({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/45089 describe('gauge chart', function indexPatternCreation() { this.tags('smoke'); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; async function initGaugeVis() { log.debug('navigateToApp visualize'); @@ -38,7 +36,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickGauge'); await PageObjects.visualize.clickGauge(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); } before(initGaugeVis); diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index ac48f78b5dde7..547ecdd598811 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -27,8 +27,6 @@ export default function ({ getService, getPageObjects }) { describe('heatmap chart', function indexPatternCreation() { this.tags('smoke'); const vizName1 = 'Visualization HeatmapChart'; - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; before(async function () { log.debug('navigateToApp visualize'); @@ -36,7 +34,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickHeatmapChart'); await PageObjects.visualize.clickHeatmapChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-Axis'); await PageObjects.visualize.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); diff --git a/test/functional/apps/visualize/_histogram_request_start.js b/test/functional/apps/visualize/_histogram_request_start.js index 76709e3ed968f..10b87d204d862 100644 --- a/test/functional/apps/visualize/_histogram_request_start.js +++ b/test/functional/apps/visualize/_histogram_request_start.js @@ -26,15 +26,13 @@ export default function ({ getService, getPageObjects }) { describe('histogram agg onSearchRequestStart', function () { before(async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickDataTable'); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split Rows'); await PageObjects.visualize.clickBucket('Split rows'); log.debug('Aggregation = Histogram'); diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 3c45063a1a16d..5917216cad40f 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -27,14 +27,12 @@ export default function ({ getService, getPageObjects }) { describe('inspector', function describeIndexTests() { this.tags('smoke'); before(async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('inspector table', function indexPatternCreation() { diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 29116b0ca56c4..cbadb7408f985 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -30,15 +30,13 @@ export default function ({ getService, getPageObjects }) { const vizName1 = 'Visualization LineChart'; const initLineChart = async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickLineChart'); await PageObjects.visualize.clickLineChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Split chart'); await PageObjects.visualize.clickBucket('Split chart'); log.debug('Aggregation = Terms'); diff --git a/test/functional/apps/visualize/_linked_saved_searches.js b/test/functional/apps/visualize/_linked_saved_searches.js index 5343873e9e0a5..4d9553b4d9262 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.js +++ b/test/functional/apps/visualize/_linked_saved_searches.js @@ -25,8 +25,6 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'visualize', 'header', 'timePicker']); describe('visualize app', function describeIndexTests() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; describe('linked saved searched', () => { @@ -45,7 +43,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickSavedSearch(savedSearchName); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await retry.waitFor('wait for count to equal 9,109', async () => { const data = await PageObjects.visualize.getTableVisData(); return data.trim() === '9,109'; @@ -53,7 +51,7 @@ export default function ({ getService, getPageObjects }) { }); it('should respect the time filter when linked to a saved search', async () => { - await PageObjects.timePicker.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-21 10:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Sep 19, 2015 @ 06:31:44.000', 'Sep 21, 2015 @ 10:00:00.000'); await retry.waitFor('wait for count to equal 3,950', async () => { const data = await PageObjects.visualize.getTableVisData(); return data.trim() === '3,950'; diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index 237ee1ef50b1e..1a4f5683f65c6 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -27,8 +27,6 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); describe('metric chart', function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; before(async function () { log.debug('navigateToApp visualize'); @@ -36,7 +34,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickMetric'); await PageObjects.visualize.clickMetric(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); it('should have inspector enabled', async function () { diff --git a/test/functional/apps/visualize/_pie_chart.js b/test/functional/apps/visualize/_pie_chart.js index b9c10aaf578bf..fc040c038afd0 100644 --- a/test/functional/apps/visualize/_pie_chart.js +++ b/test/functional/apps/visualize/_pie_chart.js @@ -25,8 +25,6 @@ export default function ({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'timePicker']); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; describe('pie chart', function () { const vizName1 = 'Visualization PieChart'; @@ -36,7 +34,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); await PageObjects.visualize.clickBucket('Split slices'); log.debug('Click aggregation Histogram'); @@ -85,7 +83,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); await PageObjects.visualize.clickBucket('Split slices'); log.debug('Click aggregation Terms'); @@ -187,7 +185,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); await PageObjects.visualize.clickBucket('Split slices'); log.debug('Click aggregation Filters'); @@ -199,8 +197,8 @@ export default function ({ getService, getPageObjects }) { log.debug('Set the 2nd filter value'); await PageObjects.visualize.setFilterAggregationValue('geo.dest:"CN"', 1); await PageObjects.visualize.clickGo(); - const emptyFromTime = '2016-09-19 06:31:44.000'; - const emptyToTime = '2016-09-23 18:31:44.000'; + const emptyFromTime = 'Sep 19, 2016 @ 06:31:44.000'; + const emptyToTime = 'Sep 23, 2016 @ 18:31:44.000'; log.debug('Switch to a different time range from \"' + emptyFromTime + '\" to \"' + emptyToTime + '\"'); await PageObjects.timePicker.setAbsoluteRange(emptyFromTime, emptyToTime); await PageObjects.visualize.waitForVisualization(); @@ -214,8 +212,10 @@ export default function ({ getService, getPageObjects }) { log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + log.debug('Set absolute time range from \"' + + PageObjects.timePicker.defaultStartTime + '\" to \"' + + PageObjects.timePicker.defaultEndTime + '\"'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); await PageObjects.visualize.clickBucket('Split slices'); log.debug('Click aggregation Histogram'); @@ -275,7 +275,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split slices'); await PageObjects.visualize.clickBucket('Split slices'); log.debug('Click aggregation Filters'); @@ -302,7 +302,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickPieChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Split chart'); await PageObjects.visualize.clickBucket('Split chart'); await PageObjects.visualize.selectAggregation('Terms'); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index d175acbd6c02e..9bbe140d22f9b 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -29,15 +29,13 @@ export default function ({ getService, getPageObjects }) { const inspector = getService('inspector'); async function initChart() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickLineChart'); await PageObjects.visualize.clickLineChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-axis'); await PageObjects.visualize.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); @@ -208,8 +206,8 @@ export default function ({ getService, getPageObjects }) { }); it('should show different labels in different timezone', async function () { - const fromTime = '2015-09-22 09:05:47.415'; - const toTime = '2015-09-22 16:08:34.554'; + const fromTime = 'Sep 22, 2015 @ 09:05:47.415'; + const toTime = 'Sep 22, 2015 @ 16:08:34.554'; // note that we're setting the absolute time range while we're in 'America/Phoenix' tz await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visualize.waitForRenderingCount(); diff --git a/test/functional/apps/visualize/_region_map.js b/test/functional/apps/visualize/_region_map.js index 11eb2e3f0ca83..e94867f903f07 100644 --- a/test/functional/apps/visualize/_region_map.js +++ b/test/functional/apps/visualize/_region_map.js @@ -23,8 +23,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { describe('vector map', function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; const inspector = getService('inspector'); const log = getService('log'); @@ -38,7 +36,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickRegionMap'); await PageObjects.visualize.clickRegionMap(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = Shape field'); await PageObjects.visualize.clickBucket('Shape field'); log.debug('Aggregation = Terms'); diff --git a/test/functional/apps/visualize/_tag_cloud.js b/test/functional/apps/visualize/_tag_cloud.js index 4ed95214550c1..5b435ef29a268 100644 --- a/test/functional/apps/visualize/_tag_cloud.js +++ b/test/functional/apps/visualize/_tag_cloud.js @@ -30,8 +30,6 @@ export default function ({ getService, getPageObjects }) { describe('tag cloud chart', function () { const vizName1 = 'Visualization tagCloud'; - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; const termsField = 'machine.ram'; before(async function () { @@ -40,7 +38,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickTagCloud'); await PageObjects.visualize.clickTagCloud(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select Tags'); await PageObjects.visualize.clickBucket('Tags'); log.debug('Click aggregation Terms'); @@ -140,7 +138,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.loadSavedVisualization(vizName1, { navigateToVisualize: false }); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.waitForVisualization(); }); diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index 0e580f6a7ab3f..f25e7e649a427 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -37,15 +37,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { await browser.setWindowSize(1280, 1000); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickTileMap'); await PageObjects.visualize.clickTileMap(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); //do not configure aggs }); @@ -62,15 +59,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { await browser.setWindowSize(1280, 1000); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickTileMap'); await PageObjects.visualize.clickTileMap(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('select bucket Geo Coordinates'); await PageObjects.visualize.clickBucket('Geo coordinates'); log.debug('Click aggregation Geohash'); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index d4896d0a0fd71..dc4b9a786eaa4 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -121,8 +121,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const value = await PageObjects.visualBuilder.getMetricValue(); expect(value).to.eql('156'); await PageObjects.visualBuilder.clickPanelOptions('metric'); - const fromTime = '2018-10-22 00:00:00.000'; - const toTime = '2018-10-28 23:59:59.999'; + const fromTime = 'Oct 22, 2018 @ 00:00:00.000'; + const toTime = 'Oct 28, 2018 @ 23:59:59.999'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights'); await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index f7b8415583a39..b7307ac9c6cab 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -43,7 +43,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await visualBuilder.resetPage(); await visualBuilder.clickMarkdown(); - await timePicker.setAbsoluteRange('2015-09-22 06:00:00.000', '2015-09-22 11:00:00.000'); + await timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 06:00:00.000', + 'Sep 22, 2015 @ 11:00:00.000' + ); }); it('should render subtabs and table variables markdown components', async () => { diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index 0b3541d798950..c096c28f7b804 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -27,7 +27,7 @@ export default function({ getPageObjects }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { describe('table', () => { beforeEach(async () => { - await visualBuilder.resetPage('2015-09-22 06:00:00.000', '2015-09-22 11:00:00.000'); + await visualBuilder.resetPage('Sep 22, 2015 @ 06:00:00.000', 'Sep 22, 2015 @ 11:00:00.000'); await visualBuilder.clickTable(); await visualBuilder.checkTableTabIsPresent(); diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index 186318ebe6325..f4fb9f13d83bd 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -25,9 +25,6 @@ export default function ({ getService, getPageObjects }) { const inspector = getService('inspector'); const log = getService('log'); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - describe('visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); @@ -64,7 +61,7 @@ export default function ({ getService, getPageObjects }) { describe('with filters', () => { before(async () => { log.debug('setAbsoluteRange'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); afterEach(async () => { diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index 3a704ab0a2026..fb08cfa898237 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -28,8 +28,6 @@ export default function ({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/22322 describe('vertical bar chart', function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; const vizName1 = 'Visualization VerticalBarChart'; const initBarChart = async () => { @@ -38,7 +36,7 @@ export default function ({ getService, getPageObjects }) { log.debug('clickVerticalBarChart'); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug('Bucket = X-Axis'); await PageObjects.visualize.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); @@ -110,8 +108,8 @@ export default function ({ getService, getPageObjects }) { }); it('should have `drop partial buckets` option', async () => { - const fromTime = '2015-09-20 06:31:44.000'; - const toTime = '2015-09-22 18:31:44.000'; + const fromTime = 'Sep 20, 2015 @ 06:31:44.000'; + const toTime = 'Sep 22, 2015 @ 18:31:44.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); diff --git a/test/functional/apps/visualize/_visualize_listing.js b/test/functional/apps/visualize/_visualize_listing.js index df4812ab3f147..71ca5e25e83e5 100644 --- a/test/functional/apps/visualize/_visualize_listing.js +++ b/test/functional/apps/visualize/_visualize_listing.js @@ -22,7 +22,8 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'header', 'common']); - describe('visualize listing page', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/40912 + describe.skip('visualize listing page', function describeIndexTests() { const vizName = 'Visualize Listing Test'; describe('create and delete', function () { diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.js b/test/functional/apps/visualize/input_control_vis/input_control_options.js index 4088ab6193a59..ef58f0da4c8f4 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.js +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.js @@ -35,7 +35,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickInputControlVis(); // set time range to time with no documents - input controls do not use time filter be default - await PageObjects.timePicker.setAbsoluteRange('2017-01-01 00:00:00.000', '2017-01-02 00:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Jan 1, 2017 @ 00:00:00.000', 'Jan 1, 2017 @ 00:00:00.000'); await PageObjects.visualize.clickVisEditorTab('controls'); await PageObjects.visualize.addInputControl(); await comboBox.set('indexPatternSelect-0', 'logstash- '); @@ -176,7 +176,7 @@ export default function ({ getService, getPageObjects }) { }); it('should re-create control when global time filter is updated', async () => { - await PageObjects.timePicker.setAbsoluteRange('2015-01-01 00:00:00.000', '2016-01-01 00:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Jan 1, 2015 @ 00:00:00.000', 'Jan 1, 2016 @ 00:00:00.000'); // Expect control to have values for selected time filter const menu = await comboBox.getOptionsList('listControlSelect0'); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 2460422722835..49e6bd02d681f 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -527,20 +527,18 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } async setTimepickerInHistoricalDataRange() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); } async setTimepickerInDataRange() { - const fromTime = '2018-01-01 00:00:00.000'; - const toTime = '2018-04-13 00:00:00.000'; + const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; + const toTime = 'Apr 13, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } async setTimepickerInLogstashDataRange() { - const fromTime = '2018-04-09 00:00:00.000'; - const toTime = '2018-04-13 00:00:00.000'; + const fromTime = 'Apr 9, 2018 @ 00:00:00.000'; + const toTime = 'Apr 13, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } diff --git a/test/functional/page_objects/time_picker.js b/test/functional/page_objects/time_picker.js index 2b4147908559a..75628c9c452c7 100644 --- a/test/functional/page_objects/time_picker.js +++ b/test/functional/page_objects/time_picker.js @@ -36,8 +36,8 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { formatDateToAbsoluteTimeString(date) { // toISOString returns dates in format 'YYYY-MM-DDTHH:mm:ss.sssZ' // Need to replace T with space and remove timezone - const dateString = date.toISOString().replace('T', ' '); - return dateString.substring(0, 23); + const DEFAULT_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; + return moment(date).format(DEFAULT_DATE_FORMAT); } async getTimePickerPanel() { @@ -74,8 +74,8 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { } /** - * @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS - * @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS + * @param {String} fromTime MMM D, YYYY @ HH:mm:ss.SSS + * @param {String} toTime MMM D, YYYY @ HH:mm:ss.SSS */ async setAbsoluteRange(fromTime, toTime) { log.debug(`Setting absolute range to ${fromTime} to ${toTime}`); @@ -112,6 +112,13 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + get defaultStartTime() { return 'Sep 19, 2015 @ 06:31:44.000'; } + get defaultEndTime() { return 'Sep 23, 2015 @ 18:31:44.000'; } + + async setDefaultAbsoluteRange() { + await this.setAbsoluteRange(this.defaultStartTime, this.defaultEndTime); + } + async isQuickSelectMenuOpen() { return await testSubjects.exists('superDatePickerQuickMenu'); } @@ -214,7 +221,7 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { } async getTimeDurationInHours() { - const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; + const DEFAULT_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; const { start, end } = await this.getTimeConfigAsAbsoluteTimes(); const startMoment = moment(start, DEFAULT_DATE_FORMAT); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 4b65de57f12d8..97d5787350376 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -43,8 +43,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro class VisualBuilderPage { public async resetPage( - fromTime = '2015-09-19 06:31:44.000', - toTime = '2015-09-22 18:31:44.000' + fromTime = 'Sep 19, 2015 @ 06:31:44.000', + toTime = 'Sep 22, 2015 @ 18:31:44.000' ) { await PageObjects.common.navigateToUrl('visualize', 'create?type=metrics'); log.debug('Set absolute time range from "' + fromTime + '" to "' + toTime + '"'); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index a8ce4270d4205..ab686f4d5ffec 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -470,7 +470,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ); } - public async executeAsync(fn: string | ((...args: any[]) => R), ...args: any[]): Promise { + public async executeAsync( + fn: string | ((...args: any[]) => Promise), + ...args: any[] + ): Promise { return await driver.executeAsyncScript( fn, ...cloneDeep(args, arg => { diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index b30a0e50886d1..380c33e93ad90 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -38,6 +38,17 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { const coveragePrefix = 'coveragejson:'; const coverageDir = resolve(__dirname, '../../../../target/kibana-coverage/functional'); let logSubscription: undefined | Rx.Subscription; + type BrowserStorage = 'sessionStorage' | 'localStorage'; + + const clearBrowserStorage = async (storageType: BrowserStorage) => { + try { + await driver.executeScript(`window.${storageType}.clear();`); + } catch (error) { + if (!error.message.includes(`Failed to read the '${storageType}' property from 'Window'`)) { + throw error; + } + } + }; const { driver, By, until, consoleLog$ } = await initWebDriver( log, @@ -128,8 +139,8 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { .manage() .window() .setRect({ width, height }); - await driver.executeScript('window.sessionStorage.clear();'); - await driver.executeScript('window.localStorage.clear();'); + await clearBrowserStorage('sessionStorage'); + await clearBrowserStorage('localStorage'); }); lifecycle.on('cleanup', async () => { diff --git a/test/interpreter_functional/README.md b/test/interpreter_functional/README.md index 336bfe3405a01..73df0ce4c9f04 100644 --- a/test/interpreter_functional/README.md +++ b/test/interpreter_functional/README.md @@ -3,7 +3,7 @@ This folder contains interpreter functional tests. Add new test suites into the `test_suites` folder and reference them from the -`config.js` file. These test suites work the same as regular functional test. +`config.ts` file. These test suites work the same as regular functional test. ## Run the test @@ -11,17 +11,17 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/interpreter_functional/config.js +node scripts/functional_tests_server.js --config test/interpreter_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts ``` # Writing tests -Look into test_suites/run_pipeline/basic.js for examples +Look into test_suites/run_pipeline/basic.ts for examples to update baseline screenshots and snapshots run with: ``` -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js --updateBaselines +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts --updateBaselines ``` \ No newline at end of file diff --git a/test/interpreter_functional/config.js b/test/interpreter_functional/config.js deleted file mode 100644 index e8700262e273a..0000000000000 --- a/test/interpreter_functional/config.js +++ /dev/null @@ -1,56 +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 path from 'path'; -import fs from 'fs'; - -export default async function ({ readConfigFile }) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); - - // Find all folders in ./plugins since we treat all them as plugin folder - const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); - const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()); - - return { - testFiles: [ - require.resolve('./test_suites/run_pipeline'), - ], - services: functionalConfig.get('services'), - pageObjects: functionalConfig.get('pageObjects'), - servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), - apps: functionalConfig.get('apps'), - esArchiver: { - directory: path.resolve(__dirname, '../es_archives') - }, - snapshots: { - directory: path.resolve(__dirname, 'snapshots'), - }, - junit: { - reportName: 'Interpreter Functional Tests', - }, - kbnTestServer: { - ...functionalConfig.get('kbnTestServer'), - serverArgs: [ - ...functionalConfig.get('kbnTestServer.serverArgs'), - ...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`), - ], - }, - }; -} diff --git a/test/interpreter_functional/config.ts b/test/interpreter_functional/config.ts new file mode 100644 index 0000000000000..0fe7df4d50715 --- /dev/null +++ b/test/interpreter_functional/config.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import path from 'path'; +import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + // Find all folders in ./plugins since we treat all them as plugin folder + const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); + const plugins = allFiles.filter(file => + fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory() + ); + + return { + testFiles: [require.resolve('./test_suites/run_pipeline')], + services: functionalConfig.get('services'), + pageObjects: functionalConfig.get('pageObjects'), + servers: functionalConfig.get('servers'), + esTestCluster: functionalConfig.get('esTestCluster'), + apps: functionalConfig.get('apps'), + esArchiver: { + directory: path.resolve(__dirname, '../es_archives'), + }, + snapshots: { + directory: path.resolve(__dirname, 'snapshots'), + }, + junit: { + reportName: 'Interpreter Functional Tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + ...plugins.map( + pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` + ), + ], + }, + }; +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js deleted file mode 100644 index 95d6a555ebcf0..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js +++ /dev/null @@ -1,39 +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. - */ - -export default function (kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Run Pipeline', - description: 'This is a sample plugin to test running pipeline expressions', - main: 'plugins/kbn_tp_run_pipeline/app', - } - }, - - init(server) { - // The following lines copy over some configuration variables from Kibana - // to this plugin. This will be needed when embedding visualizations, so that e.g. - // region map is able to get its configuration. - server.injectUiAppVars('kbn_tp_run_pipeline', async () => { - return await server.getInjectedUiAppVars('kibana'); - }); - } - }); -} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts new file mode 100644 index 0000000000000..1d5564ec06e4e --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Legacy } from 'kibana'; +import { + ArrayOrItem, + LegacyPluginApi, + LegacyPluginSpec, + LegacyPluginOptions, +} from 'src/legacy/plugin_discovery/types'; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + const pluginSpec: Partial = { + id: 'kbn_tp_run_pipeline', + uiExports: { + app: { + title: 'Run Pipeline', + description: 'This is a sample plugin to test running pipeline expressions', + main: 'plugins/kbn_tp_run_pipeline/legacy', + }, + }, + + init(server: Legacy.Server) { + // The following lines copy over some configuration variables from Kibana + // to this plugin. This will be needed when embedding visualizations, so that e.g. + // region map is able to get its configuration. + server.injectUiAppVars('kbn_tp_run_pipeline', async () => { + return server.getInjectedUiAppVars('kibana'); + }); + }, + }; + return new kibana.Plugin(pluginSpec); +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index da1bb597f5730..97ad71eaddd7c 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,8 +7,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.9.0", - "react": "^16.8.0", - "react-dom": "^16.8.0" + "@elastic/eui": "16.0.0", + "react": "^16.8.6", + "react-dom": "^16.8.6" } } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js deleted file mode 100644 index b0db26c0c6743..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ /dev/null @@ -1,86 +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 { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { registries } from 'plugins/interpreter/registries'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -// These are all the required uiExports you need to import in case you want to embed visualizations. -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visEditorTypes'; -import 'uiExports/visualize'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/fieldFormats'; -import 'uiExports/search'; - -import { Main } from './components/main'; - -const app = uiModules.get('apps/kbnRunPipelinePlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -import { fromExpression } from '@kbn/interpreter/common'; -import { getInterpreter } from '../../../../../src/legacy/core_plugins/interpreter/public/interpreter'; - -const runPipeline = async (expression, context, handlers) => { - const ast = fromExpression(expression); - const { interpreter } = await getInterpreter(); - const pipelineResponse = await interpreter.interpretAst(ast, context, handlers); - return pipelineResponse; -}; - - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(
, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('kbnRunPipelinePlugin', RootController); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js deleted file mode 100644 index 62ba8dd16fef4..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js +++ /dev/null @@ -1,103 +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 { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, -} from '@elastic/eui'; - -class Main extends React.Component { - - chartDiv = React.createRef(); - exprDiv = React.createRef(); - - constructor(props) { - super(props); - - this.state = { - expression: '', - }; - - window.runPipeline = async (expression, context = {}, initialContext = {}) => { - this.setState({ expression }); - const adapters = { - requests: new props.RequestAdapter(), - data: new props.DataAdapter(), - }; - return await props.runPipeline(expression, context, { - inspectorAdapters: adapters, - getInitialContext: () => initialContext, - }); - }; - - const handlers = { - onDestroy: () => { return; }, - }; - - window.renderPipelineResponse = async (context = {}) => { - return new Promise(resolve => { - if (context.type !== 'render') { - this.setState({ expression: 'Expression did not return render type!\n\n' + JSON.stringify(context) }); - return resolve(); - } - const renderer = props.registries.renderers.get(context.as); - if (!renderer) { - this.setState({ expression: 'Renderer was not found in registry!\n\n' + JSON.stringify(context) }); - return resolve(); - } - const renderCompleteHandler = () => { - resolve('render complete'); - this.chartDiv.removeEventListener('renderComplete', renderCompleteHandler); - }; - this.chartDiv.addEventListener('renderComplete', renderCompleteHandler); - renderer.render(this.chartDiv, context.value, handlers); - }); - - }; - - } - - - render() { - const pStyle = { - display: 'flex', - width: '100%', - height: '300px' - }; - - return ( - - - - - runPipeline tests are running ... - -
this.chartDiv = ref} style={pStyle}/> -
{this.state.expression}
- - - - ); - } -} - -export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts new file mode 100644 index 0000000000000..c4cc7175d6157 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './np_ready'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts new file mode 100644 index 0000000000000..39ce2b3077c96 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { npSetup, npStart } from 'ui/new_platform'; + +import { plugin } from './np_ready'; + +// This is required so some default styles and required scripts/Angular modules are loaded, +// or the timezone setting is correctly applied. +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +// Used for kibana_context function + +import 'uiExports/savedObjectTypes'; +import 'uiExports/interpreter'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx new file mode 100644 index 0000000000000..f47a7c3a256f0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx @@ -0,0 +1,28 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { Main } from './components/main'; + +export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { + render(
, element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx new file mode 100644 index 0000000000000..c091765619a19 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; +import { first } from 'rxjs/operators'; +import { + RequestAdapter, + DataAdapter, +} from '../../../../../../../../src/plugins/inspector/public/adapters'; +import { + Adapters, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from '../../types'; +import { getExpressions } from '../../services'; + +declare global { + interface Window { + runPipeline: ( + expressions: string, + context?: Context, + initialContext?: Context + ) => ReturnType; + renderPipelineResponse: (context?: Context) => Promise; + } +} + +interface State { + expression: string; +} + +class Main extends React.Component<{}, State> { + chartRef = React.createRef(); + + constructor(props: {}) { + super(props); + + this.state = { + expression: '', + }; + + window.runPipeline = async ( + expression: string, + context: Context = {}, + initialContext: Context = {} + ) => { + this.setState({ expression }); + const adapters: Adapters = { + requests: new RequestAdapter(), + data: new DataAdapter(), + }; + return getExpressions() + .execute(expression, { + inspectorAdapters: adapters, + context, + // TODO: naming / typing is confusing and doesn't match here + // searchContext is also a way to set initialContext and Context can't be set to SearchContext + searchContext: initialContext as any, + }) + .getData(); + }; + + let lastRenderHandler: ExpressionRenderHandler; + window.renderPipelineResponse = async (context = {}) => { + if (lastRenderHandler) { + lastRenderHandler.destroy(); + } + + lastRenderHandler = getExpressions().render(this.chartRef.current!, context); + const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); + + if (typeof renderResult === 'object' && renderResult.type === 'error') { + this.setState({ + expression: 'Render error!\n\n' + JSON.stringify(renderResult.error), + }); + } + + return renderResult; + }; + } + + render() { + const pStyle = { + display: 'flex', + width: '100%', + height: '300px', + }; + + return ( + + + + runPipeline tests are running ... +
+
{this.state.expression}
+ + + + ); + } +} + +export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts new file mode 100644 index 0000000000000..d7a764b581c01 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; +import { Plugin, StartDeps } from './plugin'; +export { StartDeps }; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new Plugin(initializerContext); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..348ba215930b0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts @@ -0,0 +1,45 @@ +/* + * 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 { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { ExpressionsStart } from './types'; +import { setExpressions } from './services'; + +export interface StartDeps { + expressions: ExpressionsStart; +} + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup({ application }: CoreSetup) { + application.register({ + id: 'kbn_tp_run_pipeline', + title: 'Run Pipeline', + async mount(context, params) { + const { renderApp } = await import('./app/app'); + return renderApp(context, params); + }, + }); + } + + public start(start: CoreStart, { expressions }: StartDeps) { + setExpressions(expressions); + } +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts new file mode 100644 index 0000000000000..657d8d5150c3a --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -0,0 +1,23 @@ +/* + * 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 { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { ExpressionsStart } from './types'; + +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts new file mode 100644 index 0000000000000..082bb47d80066 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from 'src/plugins/expressions/public'; + +import { Adapters } from 'src/plugins/inspector/public'; + +export { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, + Adapters, +}; diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 376d2b54bd08d..87f3173d56024 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 073952ac6c969..d860bb73521ce 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index f62967f95a2fa..43943b1e6c46c 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png new file mode 100644 index 0000000000000..795f2f7c832f3 Binary files /dev/null and b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png index 1db494849a23d..6578f8e30415d 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage.png b/test/interpreter_functional/screenshots/baseline/metric_percentage.png index 8e2a52bf44c1b..580889bb7deaf 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index b5bd2e1dd8839..916a284433874 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index f9367ffefdf13..9815f25d00b16 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index 376d2b54bd08d..87f3173d56024 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 36c7d6c5e1013..03ffc7ac7b1a5 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 7b8288a8bf908..3a7315df405df 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index e40c1840d29ca..795f2f7c832f3 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 7cca731c267ba..5cdce69296673 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index af5eaba74b219..394b5585097a2 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test0.json b/test/interpreter_functional/snapshots/baseline/combined_test0.json index 83aea519a9771..2af0407f0d521 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test0.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test0.json @@ -1 +1 @@ -{"type":"kibana_context"} \ No newline at end of file +{"filters":null,"query":null,"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test1.json b/test/interpreter_functional/snapshots/baseline/combined_test1.json index 7c8ad34057894..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test1.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test1.json @@ -1 +1 @@ -{"filters":[],"query":[],"type":"kibana_context"} \ No newline at end of file +{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 2a2d8dff98b8f..310377eadd165 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 2a2d8dff98b8f..310377eadd165 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index ebed9f8e47435..26f111e9edcf9 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json new file mode 100644 index 0000000000000..fa5892190e5ba --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json @@ -0,0 +1 @@ +"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index ac4860c6735bf..7f64f97845191 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ae27e624df5bb..ed8b0b258fd90 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json new file mode 100644 index 0000000000000..8a349aa5df060 --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json new file mode 100644 index 0000000000000..310377eadd165 --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json new file mode 100644 index 0000000000000..c1e429508c37f --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test0.json b/test/interpreter_functional/snapshots/baseline/step_output_test0.json index 83aea519a9771..2af0407f0d521 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test0.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test0.json @@ -1 +1 @@ -{"type":"kibana_context"} \ No newline at end of file +{"filters":null,"query":null,"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test1.json b/test/interpreter_functional/snapshots/baseline/step_output_test1.json index 7c8ad34057894..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test1.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test1.json @@ -1 +1 @@ -{"filters":[],"query":[],"type":"kibana_context"} \ No newline at end of file +{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 2a2d8dff98b8f..310377eadd165 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json index ae78b3321f0be..46b52a7b3eaae 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json @@ -1 +1 @@ -{"error":{"message":"[tagcloud] > [vis_dimension] > Objects must have a type property","stack":"Error: [vis_dimension] > Objects must have a type property\n at getType (http://localhost:5620/bundles/commons.bundle.js:15876:27)\n at cast (http://localhost:5620/bundles/commons.bundle.js:15683:46)\n at _callee3$ (http://localhost:5620/bundles/commons.bundle.js:31552:35)\n at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40)\n at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22)\n at Generator.prototype.(anonymous function) [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21)\n at asyncGeneratorStep (http://localhost:5620/bundles/commons.bundle.js:31397:103)\n at _next (http://localhost:5620/bundles/commons.bundle.js:31399:194)\n at http://localhost:5620/bundles/commons.bundle.js:31399:364\n at new Promise ()"},"type":"error"} \ No newline at end of file +"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test0.json b/test/interpreter_functional/snapshots/session/combined_test0.json new file mode 100644 index 0000000000000..2af0407f0d521 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/combined_test0.json @@ -0,0 +1 @@ +{"filters":null,"query":null,"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test1.json b/test/interpreter_functional/snapshots/session/combined_test1.json new file mode 100644 index 0000000000000..8f00d72df8ab3 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/combined_test1.json @@ -0,0 +1 @@ +{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json new file mode 100644 index 0000000000000..98c7844e41f19 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -0,0 +1 @@ +{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json new file mode 100644 index 0000000000000..310377eadd165 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json new file mode 100644 index 0000000000000..310377eadd165 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json new file mode 100644 index 0000000000000..26f111e9edcf9 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_invalid_data.json b/test/interpreter_functional/snapshots/session/metric_invalid_data.json new file mode 100644 index 0000000000000..fa5892190e5ba --- /dev/null +++ b/test/interpreter_functional/snapshots/session/metric_invalid_data.json @@ -0,0 +1 @@ +"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json new file mode 100644 index 0000000000000..7f64f97845191 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage.json b/test/interpreter_functional/snapshots/session/metric_percentage.json new file mode 100644 index 0000000000000..e171a65be8bab --- /dev/null +++ b/test/interpreter_functional/snapshots/session/metric_percentage.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json new file mode 100644 index 0000000000000..ed8b0b258fd90 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json new file mode 100644 index 0000000000000..8a349aa5df060 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json new file mode 100644 index 0000000000000..310377eadd165 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json new file mode 100644 index 0000000000000..c1e429508c37f --- /dev/null +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test0.json b/test/interpreter_functional/snapshots/session/step_output_test0.json new file mode 100644 index 0000000000000..2af0407f0d521 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/step_output_test0.json @@ -0,0 +1 @@ +{"filters":null,"query":null,"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test1.json b/test/interpreter_functional/snapshots/session/step_output_test1.json new file mode 100644 index 0000000000000..8f00d72df8ab3 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/step_output_test1.json @@ -0,0 +1 @@ +{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json new file mode 100644 index 0000000000000..98c7844e41f19 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -0,0 +1 @@ +{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json new file mode 100644 index 0000000000000..310377eadd165 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json new file mode 100644 index 0000000000000..1325c7fbed03e --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json new file mode 100644 index 0000000000000..2b063b518665a --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json new file mode 100644 index 0000000000000..46b52a7b3eaae --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json @@ -0,0 +1 @@ +"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json new file mode 100644 index 0000000000000..6152fd406961f --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json new file mode 100644 index 0000000000000..e4c6b09a264dd --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.js deleted file mode 100644 index 1cb064c2f5e56..0000000000000 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ /dev/null @@ -1,113 +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 expect from '@kbn/expect'; -import { expectExpressionProvider } from './helpers'; - -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline -// test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; - describe('basic visualize loader pipeline expression tests', () => { - before(() => { - expectExpression = expectExpressionProvider({ getService, updateBaselines }); - }); - - // we should not use this for tests like the ones below. this should be unit tested. - // - tests against a single function could easily be written as unit tests (and should be) - describe('kibana function', () => { - it('returns kibana_context', async () => { - const result = await expectExpression('returns_kibana_context', 'kibana').getResponse(); - expect(result).to.have.property('type', 'kibana_context'); - }); - - it('correctly sets timeRange', async () => { - const result = await expectExpression('correctly_sets_timerange', 'kibana', {}, { timeRange: 'test' }).getResponse(); - expect(result).to.have.property('timeRange', 'test'); - }); - }); - - // rather we want to use this to do integration tests. - // Failing on chromedriver 76 - // https://github.com/elastic/kibana/issues/42842 - describe.skip('full expression', () => { - const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ - {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, - {"id":"2","enabled":true,"type":"terms","schema":"segment","params": - {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} - }]' | - metricVis metric={visdimension 1 format="number"} bucket={visdimension 0} - `; - - // we can execute an expression and validate the result manually: - // const response = await expectExpression(expression).getResponse(); - // expect(response).... - - // we can also do snapshot comparison of result of our expression - // to update the snapshots run the tests with --updateBaselines - it ('runs the expression and compares final output', async () => { - await expectExpression('final_output_test', expression).toMatchSnapshot(); - }); - - // its also possible to check snapshot at every step of expression (after execution of each function) - it ('runs the expression and compares output at every step', async () => { - await expectExpression('step_output_test', expression).steps.toMatchSnapshot(); - }); - - // and we can do screenshot comparison of the rendered output of expression (if expression returns renderable) - it ('runs the expression and compares screenshots', async () => { - await expectExpression('final_screenshot_test', expression).toMatchScreenshot(); - }); - - // it is also possible to combine different checks - it ('runs the expression and combines different checks', async () => { - await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot(); - }); - }); - - // if we want to do multiple different tests using the same data, or reusing a part of expression its - // possible to retrieve the intermediate result and reuse it in later expressions - describe.skip('reusing partial results', () => { - it ('does some screenshot comparisons', async () => { - const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ - {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, - {"id":"2","enabled":true,"type":"terms","schema":"segment","params": - {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} - }]'`; - // we execute the part of expression that fetches the data and store its response - const context = await expectExpression('partial_test', expression).getResponse(); - - // we reuse that response to render 3 different charts and compare screenshots with baselines - const tagCloudExpr = - `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await expectExpression('partial_test_1', tagCloudExpr, context).toMatchScreenshot(); - - const metricExpr = - `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await expectExpression('partial_test_2', metricExpr, context).toMatchScreenshot(); - - // todo: regionmap doesn't correctly signal when its done rendering (base layer might not yet be loaded) - // const regionMapExpr = - // `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; - // await expectExpression('partial_test_3', regionMapExpr, context).toMatchScreenshot(); - }); - }); - }); -} diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.ts b/test/interpreter_functional/test_suites/run_pipeline/basic.ts new file mode 100644 index 0000000000000..77853b0bcd6a4 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline +// test plugin to write autmated tests for interprete +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('basic visualize loader pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + // we should not use this for tests like the ones below. this should be unit tested. + // - tests against a single function could easily be written as unit tests (and should be) + describe('kibana function', () => { + it('returns kibana_context', async () => { + const result = await expectExpression('returns_kibana_context', 'kibana').getResponse(); + expect(result).to.have.property('type', 'kibana_context'); + }); + + it('correctly sets timeRange', async () => { + const result = await expectExpression( + 'correctly_sets_timerange', + 'kibana', + {}, + { timeRange: 'test' } + ).getResponse(); + expect(result).to.have.property('timeRange', 'test'); + }); + }); + + // rather we want to use this to do integration tests. + describe('full expression', () => { + const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ + {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, + {"id":"2","enabled":true,"type":"terms","schema":"segment","params": + {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} + }]' | + metricVis metric={visdimension 1 format="number"} bucket={visdimension 0} + `; + + // we can execute an expression and validate the result manually: + // const response = await expectExpression(expression).getResponse(); + // expect(response).... + + // we can also do snapshot comparison of result of our expression + // to update the snapshots run the tests with --updateBaselines + it('runs the expression and compares final output', async () => { + await expectExpression('final_output_test', expression).toMatchSnapshot(); + }); + + // its also possible to check snapshot at every step of expression (after execution of each function) + it('runs the expression and compares output at every step', async () => { + await expectExpression('step_output_test', expression).steps.toMatchSnapshot(); + }); + + // and we can do screenshot comparison of the rendered output of expression (if expression returns renderable) + it('runs the expression and compares screenshots', async () => { + await expectExpression('final_screenshot_test', expression).toMatchScreenshot(); + }); + + // it is also possible to combine different checks + it('runs the expression and combines different checks', async () => { + await ( + await expectExpression('combined_test', expression).steps.toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); + + // if we want to do multiple different tests using the same data, or reusing a part of expression its + // possible to retrieve the intermediate result and reuse it in later expressions + describe('reusing partial results', () => { + it('does some screenshot comparisons', async () => { + const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ + {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, + {"id":"2","enabled":true,"type":"terms","schema":"segment","params": + {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} + }]'`; + // we execute the part of expression that fetches the data and store its response + const context = await expectExpression('partial_test', expression).getResponse(); + + // we reuse that response to render 3 different charts and compare screenshots with baselines + const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); + + const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); + + const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + await ( + await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.js b/test/interpreter_functional/test_suites/run_pipeline/helpers.js deleted file mode 100644 index 6186720b617af..0000000000000 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.js +++ /dev/null @@ -1,129 +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 expect from '@kbn/expect'; - -// helper for testing interpreter expressions -export const expectExpressionProvider = ({ getService, updateBaselines }) => { - const browser = getService('browser'); - const screenshot = getService('screenshots'); - const snapshots = getService('snapshots'); - const log = getService('log'); - const testSubjects = getService('testSubjects'); - /** - * returns a handler object to test a given expression - * @name: name of the test - * @expression: expression to execute - * @context: context provided to the expression - * @initialContext: initialContext provided to the expression - * @returns handler object - */ - return (name, expression, context = {}, initialContext = {}) => { - log.debug(`executing expression ${expression}`); - const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast - let responsePromise; - - const handler = { - /** - * checks if provided object matches expression result - * @param result: expected expression result - * @returns {Promise} - */ - toReturn: async result => { - const pipelineResponse = await handler.getResponse(); - expect(pipelineResponse).to.eql(result); - }, - /** - * returns expression response - * @returns {*} - */ - getResponse: () => { - if (!responsePromise) responsePromise = handler.runExpression(); - return responsePromise; - }, - /** - * runs the expression and returns the result - * @param step: expression to execute - * @param stepContext: context to provide to expression - * @returns {Promise<*>} result of running expression - */ - runExpression: async (step, stepContext) => { - log.debug(`running expression ${step || expression}`); - const promise = browser.executeAsync((expression, context, initialContext, done) => { - if (!context) context = {}; - if (!context.type) context.type = 'null'; - window.runPipeline(expression, context, initialContext).then(result => { - done(result); - }); - }, step || expression, stepContext || context, initialContext); - return await promise; - }, - steps: { - /** - * does a snapshot comparison between result of every function in the expression and the baseline - * @returns {Promise} - */ - toMatchSnapshot: async () => { - let lastResponse; - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - lastResponse = await handler.runExpression(step, lastResponse); - const diff = await snapshots.compareAgainstBaseline(name + i, lastResponse, updateBaselines); - expect(diff).to.be.lessThan(0.05); - } - if (!responsePromise) { - responsePromise = new Promise(resolve => { - resolve(lastResponse); - }); - } - return handler; - }, - }, - /** - * does a snapshot comparison between result of running the expression and baseline - * @returns {Promise} - */ - toMatchSnapshot: async () => { - const pipelineResponse = await handler.getResponse(); - await snapshots.compareAgainstBaseline(name, pipelineResponse, updateBaselines); - return handler; - }, - /** - * does a screenshot comparison between result of rendering expression and baseline - * @returns {Promise} - */ - toMatchScreenshot: async () => { - const pipelineResponse = await handler.getResponse(); - const result = await browser.executeAsync((context, done) => { - window.renderPipelineResponse(context).then(result => { - done(result); - }); - }, pipelineResponse); - log.debug('response of rendering: ', result); - - const chartEl = await testSubjects.find('pluginChart'); - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines, chartEl); - expect(percentDifference).to.be.lessThan(0.1); - return handler; - } - }; - - return handler; - }; -}; diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts new file mode 100644 index 0000000000000..e1ec18fae5e3a --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { + ExpressionDataHandler, + RenderResult, + Context, +} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; + +type UnWrapPromise = T extends Promise ? U : T; +export type ExpressionResult = UnWrapPromise>; + +export type ExpectExpression = ( + name: string, + expression: string, + context?: Context, + initialContext?: Context +) => ExpectExpressionHandler; + +export interface ExpectExpressionHandler { + toReturn: (expectedResult: ExpressionResult) => Promise; + getResponse: () => Promise; + runExpression: (step?: string, stepContext?: Context) => Promise; + steps: { + toMatchSnapshot: () => Promise; + }; + toMatchSnapshot: () => Promise; + toMatchScreenshot: () => Promise; +} + +// helper for testing interpreter expressions +export function expectExpressionProvider({ + getService, + updateBaselines, +}: Pick & { updateBaselines: boolean }): ExpectExpression { + const browser = getService('browser'); + const screenshot = getService('screenshots'); + const snapshots = getService('snapshots'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + + /** + * returns a handler object to test a given expression + * @name: name of the test + * @expression: expression to execute + * @context: context provided to the expression + * @initialContext: initialContext provided to the expression + * @returns handler object + */ + return ( + name: string, + expression: string, + context: Context = {}, + initialContext: Context = {} + ): ExpectExpressionHandler => { + log.debug(`executing expression ${expression}`); + const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast + let responsePromise: Promise; + + const handler: ExpectExpressionHandler = { + /** + * checks if provided object matches expression result + * @param result: expected expression result + * @returns {Promise} + */ + toReturn: async (expectedResult: ExpressionResult) => { + const pipelineResponse = await handler.getResponse(); + expect(pipelineResponse).to.eql(expectedResult); + }, + /** + * returns expression response + * @returns {*} + */ + getResponse: () => { + if (!responsePromise) responsePromise = handler.runExpression(); + return responsePromise; + }, + /** + * runs the expression and returns the result + * @param step: expression to execute + * @param stepContext: context to provide to expression + * @returns {Promise<*>} result of running expression + */ + runExpression: async ( + step: string = expression, + stepContext: Context = context + ): Promise => { + log.debug(`running expression ${step || expression}`); + return browser.executeAsync( + ( + _expression: string, + _currentContext: Context & { type: string }, + _initialContext: Context, + done: (expressionResult: ExpressionResult) => void + ) => { + if (!_currentContext) _currentContext = { type: 'null' }; + if (!_currentContext.type) _currentContext.type = 'null'; + return window + .runPipeline(_expression, _currentContext, _initialContext) + .then(expressionResult => { + done(expressionResult); + return expressionResult; + }); + }, + step, + stepContext, + initialContext + ); + }, + steps: { + /** + * does a snapshot comparison between result of every function in the expression and the baseline + * @returns {Promise} + */ + toMatchSnapshot: async () => { + let lastResponse: ExpressionResult; + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + lastResponse = await handler.runExpression(step, lastResponse!); + const diff = await snapshots.compareAgainstBaseline( + name + i, + toSerializable(lastResponse!), + updateBaselines + ); + expect(diff).to.be.lessThan(0.05); + } + if (!responsePromise) { + responsePromise = Promise.resolve(lastResponse!); + } + return handler; + }, + }, + /** + * does a snapshot comparison between result of running the expression and baseline + * @returns {Promise} + */ + toMatchSnapshot: async () => { + const pipelineResponse = await handler.getResponse(); + await snapshots.compareAgainstBaseline( + name, + toSerializable(pipelineResponse), + updateBaselines + ); + return handler; + }, + /** + * does a screenshot comparison between result of rendering expression and baseline + * @returns {Promise} + */ + toMatchScreenshot: async () => { + const pipelineResponse = await handler.getResponse(); + log.debug('starting to render'); + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: RenderResult) => void) => + window.renderPipelineResponse(_context).then(renderResult => { + done(renderResult); + return renderResult; + }), + pipelineResponse + ); + log.debug('response of rendering: ', result); + + const chartEl = await testSubjects.find('pluginChart'); + const percentDifference = await screenshot.compareAgainstBaseline( + name, + updateBaselines, + chartEl + ); + expect(percentDifference).to.be.lessThan(0.1); + return handler; + }, + }; + + return handler; + }; + + function toSerializable(response: ExpressionResult) { + if (response.error) { + // in case of error, pass through only message to the snapshot + // as error could be expected and stack trace shouldn't be part of the snapshot + return response.error.message; + } + return response; + } +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.js deleted file mode 100644 index ebc0568ebb955..0000000000000 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ /dev/null @@ -1,45 +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. - */ - -export default function ({ getService, getPageObjects, loadTestFile }) { - const browser = getService('browser'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const appsMenu = getService('appsMenu'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header']); - - describe.skip('runPipeline', function () { - this.tags(['skipFirefox']); - - before(async () => { - await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' }); - await browser.setWindowSize(1300, 900); - await PageObjects.common.navigateToApp('settings'); - await appsMenu.clickLink('Run Pipeline'); - await testSubjects.find('pluginContent'); - }); - - loadTestFile(require.resolve('./basic')); - loadTestFile(require.resolve('./tag_cloud')); - loadTestFile(require.resolve('./metric')); - }); -} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts new file mode 100644 index 0000000000000..031a0e3576ccc --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -0,0 +1,50 @@ +/* + * 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 { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('runPipeline', function() { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); + await kibanaServer.uiSettings.replace({ + 'dateFormat:tz': 'Australia/North', + defaultIndex: 'logstash-*', + }); + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + await appsMenu.clickLink('Run Pipeline'); + await testSubjects.find('pluginContent'); + }); + + loadTestFile(require.resolve('./basic')); + loadTestFile(require.resolve('./tag_cloud')); + loadTestFile(require.resolve('./metric')); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.js deleted file mode 100644 index b772617b53bff..0000000000000 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.js +++ /dev/null @@ -1,81 +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 { expectExpressionProvider } from './helpers'; - -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline -// test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; - describe('metricVis pipeline expression tests', () => { - before(() => { - expectExpression = expectExpressionProvider({ getService, updateBaselines }); - }); - - // we should not use this for tests like the ones below. this should be unit tested. - // - tests against a single function could easily be written as unit tests (and should be) - describe('correctly renders metric', () => { - let dataContext; - before(async () => { - const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ - {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, - {"id":"1","enabled":true,"type":"max","schema":"metric","params": - {"field":"bytes"} - }, - {"id":"2","enabled":true,"type":"terms","schema":"segment","params": - {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} - }]'`; - // we execute the part of expression that fetches the data and store its response - dataContext = await expectExpression('partial_metric_test', expression).getResponse(); - }); - - it.skip('with invalid data', async () => { - const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); - }); - - // Test fails on chromedriver 76 - // https://github.com/elastic/kibana/issues/42842 - it.skip('with single metric data', async () => { - const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - - // Test fails on chromedriver 76 - // https://github.com/elastic/kibana/issues/42842 - it.skip('with multiple metric data', async () => { - const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; - await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot(); - }); - - // Test fails on chromedriver 76 - // https://github.com/elastic/kibana/issues/42842 - it.skip('with metric and bucket data', async () => { - const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; - await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - - it('with percentage option', async () => { - const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; - await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot(); - }); - }); - }); -} diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.ts b/test/interpreter_functional/test_suites/run_pipeline/metric.ts new file mode 100644 index 0000000000000..c238bedfa28ce --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('metricVis pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders metric', () => { + let dataContext: ExpressionResult; + before(async () => { + const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ + {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, + {"id":"1","enabled":true,"type":"max","schema":"metric","params": + {"field":"bytes"} + }, + {"id":"2","enabled":true,"type":"terms","schema":"segment","params": + {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} + }]'`; + // we execute the part of expression that fetches the data and store its response + dataContext = await expectExpression('partial_metric_test', expression).getResponse(); + }); + + it('with invalid data', async () => { + const expression = 'metricVis metric={visdimension 0}'; + await ( + await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with single metric data', async () => { + const expression = 'metricVis metric={visdimension 0}'; + await ( + await expectExpression( + 'metric_single_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with multiple metric data', async () => { + const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; + await ( + await expectExpression( + 'metric_multi_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with metric and bucket data', async () => { + const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; + await ( + await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with percentage option', async () => { + const expression = + 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + await ( + await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js deleted file mode 100644 index 3bcbf8f2075ad..0000000000000 --- a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js +++ /dev/null @@ -1,72 +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 { expectExpressionProvider } from './helpers'; - -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline -// test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; - describe('tag cloud pipeline expression tests', () => { - before(() => { - expectExpression = expectExpressionProvider({ getService, updateBaselines }); - }); - - // we should not use this for tests like the ones below. this should be unit tested. - // - tests against a single function could easily be written as unit tests (and should be) - describe('correctly renders tagcloud', () => { - let dataContext; - before(async () => { - const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ - {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, - {"id":"2","enabled":true,"type":"terms","schema":"segment","params": - {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} - }]'`; - // we execute the part of expression that fetches the data and store its response - dataContext = await expectExpression('partial_tagcloud_test', expression).getResponse(); - }); - - it.skip('with invalid data', async () => { - const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); - }); - - it('with just metric data', async () => { - const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - - it('with metric and bucket data', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}'; - await (await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - - it('with font size options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; - await (await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - - it('with scale and orientation options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; - await (await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); - }); - }); - }); -} diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts new file mode 100644 index 0000000000000..2451df4db6310 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts @@ -0,0 +1,83 @@ +/* + * 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 { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('tag cloud pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders tagcloud', () => { + let dataContext: ExpressionResult; + before(async () => { + const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ + {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, + {"id":"2","enabled":true,"type":"terms","schema":"segment","params": + {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} + }]'`; + // we execute the part of expression that fetches the data and store its response + dataContext = await expectExpression('partial_tagcloud_test', expression).getResponse(); + }); + + it('with invalid data', async () => { + const expression = 'tagcloud metric={visdimension 0}'; + await ( + await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with just metric data', async () => { + const expression = 'tagcloud metric={visdimension 0}'; + await ( + await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with metric and bucket data', async () => { + const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}'; + await ( + await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with font size options', async () => { + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; + await ( + await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + + it('with scale and orientation options', async () => { + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; + await ( + await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); + }); +} diff --git a/test/plugin_functional/plugins/demo_search/server/constants.ts b/test/plugin_functional/plugins/demo_search/server/constants.ts deleted file mode 100644 index 11c258a21d5a8..0000000000000 --- a/test/plugin_functional/plugins/demo_search/server/constants.ts +++ /dev/null @@ -1,20 +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. - */ - -export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY'; diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 4d0444265825a..ca584b4b4e771 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.9.0", - "react": "^16.8.0" + "@elastic/eui": "16.0.0", + "react": "^16.8.6" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 9df9352f76fc2..71545fa582c66 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,8 +8,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.9.0", - "react": "^16.8.0" + "@elastic/eui": "16.0.0", + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index f03b3c4a1e0a5..6b82a67b9fcda 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -27,7 +27,7 @@ import { Setup as InspectorSetupContract, } from '../../../../../../../src/plugins/inspector/public'; -import { Plugin as EmbeddablePlugin, CONTEXT_MENU_TRIGGER } from './embeddable_api'; +import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; @@ -38,9 +38,13 @@ import { ContactCardEmbeddableFactory, } from './embeddable_api'; import { App } from './app'; +import { + IEmbeddableStart, + IEmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { SavedObjectFinder: React.ComponentType; @@ -49,7 +53,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; inspector: InspectorStartContract; __LEGACY: { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 054276b620907..d5c97bb212ea0 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,8 +8,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.9.0", - "react": "^16.8.0" + "@elastic/eui": "16.0.0", + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 1a7a1c973102c..27f73c0b6e20d 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -6,7 +6,7 @@ export TEST_BROWSER_HEADLESS=1 echo " -> Running mocha tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Mocha" yarn test +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser echo "" echo "" diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 18575fcb5fcff..77aeaa8af3b9f 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -29,9 +29,7 @@ export default async function ({ readConfigFile }) { return { services: { - es: commonConfig.get('services.es'), - esArchiver: commonConfig.get('services.esArchiver'), - retry: commonConfig.get('services.retry'), + ...commonConfig.get('services'), supertest: KibanaSupertestProvider, supertestWithoutAuth: KibanaSupertestWithoutAuthProvider, esSupertest: ElasticsearchSupertestProvider, diff --git a/test/visual_regression/tests/discover/chart_visualization.js b/test/visual_regression/tests/discover/chart_visualization.js index 4e1d9bf643cf6..540d95973b547 100644 --- a/test/visual_regression/tests/discover/chart_visualization.js +++ b/test/visual_regression/tests/discover/chart_visualization.js @@ -33,8 +33,6 @@ export default function ({ getService, getPageObjects }) { }; describe('discover', function describeIndexTests() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; before(async function () { log.debug('load kibana index with default index pattern'); @@ -45,7 +43,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('query', function () { @@ -132,7 +130,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); await browser.refresh(); await PageObjects.header.awaitKibanaChrome(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await visualTesting.snapshot({ diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index 39fc30230de18..dc4d0d778fc55 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -21,9 +21,9 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; // TODO: Remove once typescript definitions are in EUI declare module '@elastic/eui' { - export const EuiSideNav: React.SFC; - export const EuiDescribedFormGroup: React.SFC; - export const EuiCodeEditor: React.SFC; + export const EuiSideNav: React.FC; + export const EuiDescribedFormGroup: React.FC; + export const EuiCodeEditor: React.FC; export const Query: any; export interface EuiTableCriteria { diff --git a/x-pack/README.md b/x-pack/README.md index bd50181afee69..3f1fc819d145b 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -23,7 +23,7 @@ By default, this will also set the password for native realm accounts to the pas Examples: - Run the jest test case whose description matches 'filtering should skip values of null': - `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js` + `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js` - Run the x-pack api integration test case whose description matches the given string: `node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` `node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 9d601e680cf87..199232262773d 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -36,7 +36,10 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { `/dev-tools/jest/setup/polyfills.js`, `/dev-tools/jest/setup/enzyme.js`, ], - setupFilesAfterEnv: [`${kibanaDirectory}/src/dev/jest/setup/mocks.js`], + setupFilesAfterEnv: [ + `/dev-tools/jest/setup/setup_test.js`, + `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, + ], testMatch: ['**/*.test.{js,ts,tsx}'], transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, @@ -49,7 +52,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { ], snapshotSerializers: [ `${kibanaDirectory}/node_modules/enzyme-to-json/serializer`, - `${kibanaDirectory}/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts` + `${kibanaDirectory}/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts`, ], reporters: [ 'default', diff --git a/x-pack/dev-tools/jest/setup/setup_test.js b/x-pack/dev-tools/jest/setup/setup_test.js new file mode 100644 index 0000000000000..533ea58a561ac --- /dev/null +++ b/x-pack/dev-tools/jest/setup/setup_test.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + Global import, so we don't need to remember to import the lib in each file + https://www.npmjs.com/package/jest-styled-components#global-installation +*/ + +import 'jest-styled-components'; diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 74e24692f59f6..d3f93c29e3df8 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -8,7 +8,7 @@ require('../src/setup_node_env'); const { buildTask } = require('./tasks/build'); const { devTask } = require('./tasks/dev'); -const { testTask, testBrowserTask, testBrowserDevTask, testServerTask } = require('./tasks/test'); +const { testTask, testBrowserTask, testBrowserDevTask } = require('./tasks/test'); const { prepareTask } = require('./tasks/prepare'); // export the tasks that are runnable from the CLI @@ -17,7 +17,6 @@ module.exports = { dev: devTask, prepare: prepareTask, test: testTask, - testserver: testServerTask, testbrowser: testBrowserTask, 'testbrowser-dev': testBrowserDevTask, }; diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx index 8becf6892ff92..aab16f9d79c4b 100644 --- a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx +++ b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx @@ -16,16 +16,18 @@ export interface EuiTheme { darkMode: boolean; } -const EuiThemeProvider = ({ +const EuiThemeProvider = < + OuterTheme extends styledComponents.DefaultTheme = styledComponents.DefaultTheme +>({ darkMode = false, ...otherProps -}: ThemeProviderProps & { +}: Omit, 'theme'> & { darkMode?: boolean; - children?: React.ReactNode; }) => ( ({ + theme={(outerTheme?: OuterTheme) => ({ + ...outerTheme, eui: darkMode ? euiDarkVars : euiLightVars, darkMode, })} @@ -35,9 +37,9 @@ const EuiThemeProvider = ({ const { default: euiStyled, css, - injectGlobal, + createGlobalStyle, keyframes, withTheme, } = (styledComponents as unknown) as ThemedStyledComponentsModule; -export { css, euiStyled, EuiThemeProvider, injectGlobal, keyframes, withTheme }; +export { css, euiStyled, EuiThemeProvider, createGlobalStyle, keyframes, withTheme }; diff --git a/x-pack/legacy/common/eui_styled_components/index.ts b/x-pack/legacy/common/eui_styled_components/index.ts index 9d07b540d4974..9b3ed903627b4 100644 --- a/x-pack/legacy/common/eui_styled_components/index.ts +++ b/x-pack/legacy/common/eui_styled_components/index.ts @@ -9,12 +9,12 @@ import { euiStyled, EuiTheme, EuiThemeProvider, - injectGlobal, + createGlobalStyle, keyframes, withTheme, } from './eui_styled_components'; -export { css, euiStyled, EuiTheme, EuiThemeProvider, injectGlobal, keyframes, withTheme }; +export { css, euiStyled, EuiTheme, EuiThemeProvider, createGlobalStyle, keyframes, withTheme }; // In order to to mimic the styled-components module we need to ignore the following // eslint-disable-next-line import/no-default-export export default euiStyled; diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 456eb6732c81c..40f61d11e9ace 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -202,7 +202,7 @@ Payload: |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `DELETE /api/alert/{id}`: Delete alert @@ -246,7 +246,7 @@ Payload: |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `POST /api/alert/{id}/_enable`: Enable an alert diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index 7a1be777aff44..f840c019d5e02 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -31,7 +31,7 @@ } } }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index dc3aaaf5cf23c..08607f04a5235 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -48,7 +48,7 @@ function getMockData(overwrites: Record = {}) { alertTypeId: '123', interval: '10s', throttle: null, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -80,7 +80,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -130,25 +130,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); @@ -164,9 +164,6 @@ describe('create()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": undefined, "apiKeyOwner": undefined, "createdBy": "elastic", @@ -175,6 +172,9 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -240,7 +240,7 @@ describe('create()', () => { enabled: false, alertTypeId: '123', interval: 10000, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -263,30 +263,30 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "enabled": false, + "id": "1", + "interval": 10000, + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); - test('should validate alertTypeParams', async () => { + test('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData(); alertTypeRegistry.get.mockReturnValueOnce({ @@ -302,7 +302,7 @@ describe('create()', () => { async executor() {}, }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); @@ -337,7 +337,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -387,7 +387,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -448,7 +448,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -511,7 +511,7 @@ describe('create()', () => { ], alertTypeId: '123', name: 'abc', - alertTypeParams: { bar: true }, + params: { bar: true }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', createdBy: 'elastic', @@ -923,7 +923,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -946,24 +946,24 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -981,7 +981,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1016,7 +1016,7 @@ describe('find()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1041,31 +1041,31 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1086,7 +1086,7 @@ describe('delete()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, scheduledTaskId: 'task-123', @@ -1155,7 +1155,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1183,7 +1183,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1198,25 +1198,25 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1233,14 +1233,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": null, "apiKeyOwner": null, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1291,7 +1291,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1320,7 +1320,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1335,26 +1335,26 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1371,14 +1371,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1400,7 +1400,7 @@ describe('update()', () => { `); }); - it('should validate alertTypeParams', async () => { + it('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ id: '123', @@ -1428,7 +1428,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1443,7 +1443,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index fbdf496ebaec4..3916ec1d62b6c 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -32,7 +32,7 @@ interface ConstructorOptions { createAPIKey: () => Promise; } -interface FindOptions { +export interface FindOptions { options?: { perPage?: number; page?: number; @@ -40,6 +40,7 @@ interface FindOptions { defaultSearchOperator?: 'AND' | 'OR'; searchFields?: string[]; sortField?: string; + sortOrder?: string; hasReference?: { type: string; id: string; @@ -76,7 +77,7 @@ interface UpdateOptions { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; }; } @@ -110,7 +111,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions) { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const apiKey = await this.createAPIKey(); const username = await this.getUserName(); @@ -124,7 +125,7 @@ export class AlertsClient { apiKey: apiKey.created ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : undefined, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], }); @@ -198,7 +199,7 @@ export class AlertsClient { const apiKey = await this.createAPIKey(); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); this.validateActions(alertType, data.actions); const { actions, references } = this.extractReferences(data.actions); @@ -209,7 +210,7 @@ export class AlertsClient { { ...attributes, ...data, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, actions, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index dcc74ed9488ce..1d91d4a35d588 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -76,7 +76,7 @@ const mockedAlertTypeSavedObject = { alertTypeId: '123', interval: '10s', mutedInstanceIds: [], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -253,7 +253,7 @@ test('validates params before executing the alert type', async () => { references: [], }); await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 66d445f57fe73..051b15fc8dd8f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -94,12 +94,12 @@ export class TaskRunnerFactory { const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds }, + attributes: { params, actions, interval, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, params); // Inject ids into actions const actionsWithIds = actions.map(action => { diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts index f33746798769b..e9a61354001f1 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts @@ -61,6 +61,6 @@ test('should validate and throw error when params is invalid', () => { {} ) ).toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts index 6070f2d99b605..248d896c06ac2 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -19,6 +19,6 @@ export function validateAlertTypeParams>( try { return validator.validate(params); } catch (err) { - throw Boom.badRequest(`alertTypeParams invalid: ${err.message}`); + throw Boom.badRequest(`params invalid: ${err.message}`); } } diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index c67d1a7b32352..318dbdf068d6a 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -15,7 +15,7 @@ const mockedAlert = { name: 'abc', interval: '10s', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -57,12 +57,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "id": "123", "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -83,12 +83,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -112,12 +112,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index 65fbae7c8b298..fb82a03f172b3 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -17,7 +17,7 @@ interface ScheduleRequest extends Hapi.Request { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -41,7 +41,7 @@ export const createAlertRoute = { alertTypeId: Joi.string().required(), throttle: getDurationSchema().default(null), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 84938a0e927d1..19618bc9e39fe 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -14,7 +14,7 @@ const mockedAlert = { id: '1', alertTypeId: '1', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index ee98f7d6dd9d3..7fc3f45911010 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -17,7 +17,7 @@ const mockedResponse = { alertTypeId: '1', tags: ['foo'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -40,7 +40,7 @@ test('calls the update function with proper parameters', async () => { name: 'abc', tags: ['bar'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -74,11 +74,11 @@ test('calls the update function with proper parameters', async () => { }, }, ], - "alertTypeParams": Object { - "otherField": false, - }, "interval": "12s", "name": "abc", + "params": Object { + "otherField": false, + }, "tags": Array [ "bar", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 9c8e0296c2f78..6aeedb93a1098 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -19,7 +19,7 @@ interface UpdateRequest extends Hapi.Request { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -43,7 +43,7 @@ export const updateAlertRoute = { .items(Joi.string()) .required(), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 359b88e21cc3b..e2460c549c05d 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -65,7 +65,7 @@ export interface Alert { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; @@ -83,7 +83,7 @@ export interface RawAlert extends SavedObjectAttributes { alertTypeId: string; interval: string; actions: RawAlertAction[]; - alertTypeParams: SavedObjectAttributes; + params: SavedObjectAttributes; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 0521270a7ba74..9d82cd6b5455c 100644 --- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -102,6 +102,8 @@ exports[`Error TRANSACTION_TYPE 1`] = `"request"`; exports[`Error URL_FULL 1`] = `undefined`; +exports[`Error USER_AGENT_NAME 1`] = `undefined`; + exports[`Error USER_ID 1`] = `undefined`; exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`; @@ -206,6 +208,8 @@ exports[`Span TRANSACTION_TYPE 1`] = `undefined`; exports[`Span URL_FULL 1`] = `undefined`; +exports[`Span USER_AGENT_NAME 1`] = `undefined`; + exports[`Span USER_ID 1`] = `undefined`; exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`; @@ -310,4 +314,6 @@ exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`; exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`; +exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`; + exports[`Transaction USER_ID 1`] = `"1337"`; diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts index 52471e08b1b4d..82a679ccdd32e 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts @@ -34,6 +34,7 @@ describe('Transaction', () => { timestamp: { us: 1337 }, trace: { id: 'trace id' }, user: { id: '1337' }, + user_agent: { name: 'Other', original: 'test original' }, parent: { id: 'parentId' }, diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index 552c149ce6214..d0830337e0d35 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -10,6 +10,7 @@ export const SERVICE_NODE_NAME = 'service.node.name'; export const URL_FULL = 'url.full'; export const HTTP_REQUEST_METHOD = 'http.request.method'; export const USER_ID = 'user.id'; +export const USER_AGENT_NAME = 'user_agent.name'; export const OBSERVER_VERSION_MAJOR = 'observer.version_major'; export const OBSERVER_LISTENING = 'observer.listening'; diff --git a/x-pack/legacy/plugins/apm/common/processor_event.ts b/x-pack/legacy/plugins/apm/common/processor_event.ts index a513f62092767..83dadfc21da90 100644 --- a/x-pack/legacy/plugins/apm/common/processor_event.ts +++ b/x-pack/legacy/plugins/apm/common/processor_event.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export type ProcessorEvent = 'transaction' | 'error' | 'metric'; +export enum ProcessorEvent { + transaction = 'transaction', + error = 'error', + metric = 'metric' +} diff --git a/x-pack/legacy/plugins/apm/common/projections/errors.ts b/x-pack/legacy/plugins/apm/common/projections/errors.ts index adbd2eb1d6d27..27e1de43a1a94 100644 --- a/x-pack/legacy/plugins/apm/common/projections/errors.ts +++ b/x-pack/legacy/plugins/apm/common/projections/errors.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../server/lib/helpers/setup_request'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -16,7 +20,7 @@ export function getErrorGroupsProjection({ setup, serviceName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { const { start, end, uiFiltersES, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/common/projections/metrics.ts b/x-pack/legacy/plugins/apm/common/projections/metrics.ts index 25d1484624e15..066f5789752a7 100644 --- a/x-pack/legacy/plugins/apm/common/projections/metrics.ts +++ b/x-pack/legacy/plugins/apm/common/projections/metrics.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, PROCESSOR_EVENT, @@ -30,7 +34,7 @@ export function getMetricsProjection({ serviceName, serviceNodeName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; serviceNodeName?: string; }) { diff --git a/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts b/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts index 10ce75785c3bc..42fcdd38cc9fd 100644 --- a/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../server/lib/helpers/setup_request'; import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; @@ -14,7 +18,7 @@ export function getServiceNodesProjection({ serviceName, serviceNodeName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; serviceNodeName?: string; }) { diff --git a/x-pack/legacy/plugins/apm/common/projections/services.ts b/x-pack/legacy/plugins/apm/common/projections/services.ts index e889899e11634..3531607d59fc4 100644 --- a/x-pack/legacy/plugins/apm/common/projections/services.ts +++ b/x-pack/legacy/plugins/apm/common/projections/services.ts @@ -4,11 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupUIFilters, + SetupTimeRange +} from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; import { rangeFilter } from '../../server/lib/helpers/range_filter'; -export function getServicesProjection({ setup }: { setup: Setup }) { +export function getServicesProjection({ + setup +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { const { start, end, uiFiltersES, indices } = setup; return { diff --git a/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts index 6f7be349b0cba..abda606f69384 100644 --- a/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { omit } from 'lodash'; -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../server/lib/helpers/setup_request'; import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; @@ -14,7 +18,7 @@ export function getTransactionGroupsProjection({ setup, options }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; options: Options; }) { const transactionsProjection = getTransactionsProjection({ diff --git a/x-pack/legacy/plugins/apm/common/projections/transactions.ts b/x-pack/legacy/plugins/apm/common/projections/transactions.ts index fb249340c867c..ecbd0c8bf1a31 100644 --- a/x-pack/legacy/plugins/apm/common/projections/transactions.ts +++ b/x-pack/legacy/plugins/apm/common/projections/transactions.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../server/lib/helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -19,7 +23,7 @@ export function getTransactionsProjection({ transactionName, transactionType }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName?: string; transactionName?: string; transactionType?: string; diff --git a/x-pack/legacy/plugins/apm/common/transaction_types.ts b/x-pack/legacy/plugins/apm/common/transaction_types.ts index 4dd59af63047d..1226e926b1ee3 100644 --- a/x-pack/legacy/plugins/apm/common/transaction_types.ts +++ b/x-pack/legacy/plugins/apm/common/transaction_types.ts @@ -5,5 +5,5 @@ */ export const TRANSACTION_PAGE_LOAD = 'page-load'; -export const TRANSACTION_ROUTE_CHANGE = 'route-change'; export const TRANSACTION_REQUEST = 'request'; +export const TRANSACTION_ROUTE_CHANGE = 'route-change'; diff --git a/x-pack/legacy/plugins/apm/common/viz_colors.ts b/x-pack/legacy/plugins/apm/common/viz_colors.ts new file mode 100644 index 0000000000000..cc070005409b6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/viz_colors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +function getVizColorsForTheme(theme = lightTheme) { + return [ + theme.euiColorVis0, + theme.euiColorVis1, + theme.euiColorVis2, + theme.euiColorVis3, + theme.euiColorVis4, + theme.euiColorVis5, + theme.euiColorVis6, + theme.euiColorVis7, + theme.euiColorVis8, + theme.euiColorVis9 + ]; +} + +export function getVizColorForIndex(index = 0, theme = lightTheme) { + const colors = getVizColorsForTheme(theme); + return colors[index % colors.length]; +} diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index e2cf907a61457..1784ed22a2b4d 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import { resolve } from 'path'; -import { PluginInitializerContext } from '../../../../src/core/server'; +import { APMPluginContract } from '../../../plugins/apm/server/plugin'; import { LegacyPluginInitializer } from '../../../../src/legacy/types'; import mappings from './mappings.json'; -import { plugin } from './server/new-platform'; +import { makeApmUsageCollector } from './server/lib/apm_telemetry'; export const apm: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ @@ -68,10 +68,6 @@ export const apm: LegacyPluginInitializer = kibana => { // enable plugin enabled: Joi.boolean().default(true), - // buckets - minimumBucketSize: Joi.number().default(15), - bucketTargetCount: Joi.number().default(15), - // index patterns autocreateApmIndexPattern: Joi.boolean().default(true), @@ -112,15 +108,12 @@ export const apm: LegacyPluginInitializer = kibana => { } } }); + const { usageCollection } = server.newPlatform.setup.plugins; + makeApmUsageCollector(usageCollection, server); + const apmPlugin = server.newPlatform.setup.plugins + .apm as APMPluginContract; - const initializerContext = {} as PluginInitializerContext; - const legacySetup = { - server - }; - plugin(initializerContext).setup( - server.newPlatform.setup.core, - legacySetup - ); + apmPlugin.registerLegacyAPI({ server }); } }); }; diff --git a/x-pack/legacy/plugins/apm/mappings.json b/x-pack/legacy/plugins/apm/mappings.json index 02296606b1c01..61bc90da28756 100644 --- a/x-pack/legacy/plugins/apm/mappings.json +++ b/x-pack/legacy/plugins/apm/mappings.json @@ -61,9 +61,6 @@ }, "apm_oss.metricsIndices": { "type": "keyword" - }, - "apm_oss.apmAgentConfigurationIndex": { - "type": "keyword" } } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index c663c52d7d639..2b053274e8afe 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -122,7 +122,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` @@ -501,7 +501,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` @@ -669,18 +669,24 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` List should render with data 1`] = `
-
- - - - About to blow up! - - - -
- -
+ + About to blow up! + + + + +
+ - elasticapm.contrib.django.client.capture -
-
-
+ +
+ elasticapm.contrib.django.client.capture +
+
+ +
+
List should render with data 1`] = ` List should render with data 1`] = `
-
- - - - AssertionError: - - - -
- -
+ + AssertionError: + + + + +
+ - opbeans.views.oopsie -
-
-
+ +
+ opbeans.views.oopsie +
+
+ +
+
List should render with data 1`] = ` List should render with data 1`] = `
-
- - - - AssertionError: Bad luck! - - - -
- -
+ + AssertionError: Bad luck! + + + + +
+ - opbeans.tasks.update_stats -
-
-
+ +
+ opbeans.tasks.update_stats +
+
+ +
+ List should render with data 1`] = ` List should render with data 1`] = `
-
- - - - Customer with ID 8517 not found - - - -
- -
+ + Customer with ID 8517 not found + + + + +
+ - opbeans.views.customer -
-
-
+ +
+ opbeans.views.customer +
+
+ +
+ { +const ErrorGroupOverview: React.FC = () => { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, sortField, sortDirection } = urlParams; diff --git a/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx deleted file mode 100644 index 57a0e5ad9ddc4..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx +++ /dev/null @@ -1,49 +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 { EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import url from 'url'; -import { px, units } from '../../../style/variables'; -import { useKibanaCore } from '../../../../../observability/public'; - -const Container = styled.div` - margin: ${px(units.minus)} 0; -`; - -export const GlobalHelpExtension: React.SFC = () => { - const core = useKibanaCore(); - - return ( - - - - {i18n.translate('xpack.apm.feedbackMenu.provideFeedbackTitle', { - defaultMessage: 'Provide feedback for APM' - })} - - - - - {i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { - defaultMessage: 'Upgrade assistant' - })} - - - - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx index 035015c82a0ac..7a23c9f7de842 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); describe('Home component', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 8ddf48e79f911..41fb12be284ad 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); const coreMock = { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts deleted file mode 100644 index bb9f581129c5e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.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 { useEffect } from 'react'; -import { capabilities } from 'ui/capabilities'; -import { useKibanaCore } from '../../../../../observability/public'; - -export const useUpdateBadgeEffect = () => { - const { chrome } = useKibanaCore(); - - useEffect(() => { - const uiCapabilities = capabilities.get(); - chrome.setBadge( - !uiCapabilities.apm.save - ? { - text: i18n.translate('xpack.apm.header.badge.readOnly.text', { - defaultMessage: 'Read only' - }), - tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save' - }), - iconType: 'glasses' - } - : undefined - ); - }, [chrome]); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index d52c869b95872..18964531958f7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -191,6 +191,7 @@ export class WatcherFlyout extends Component< ) as string; return createErrorGroupWatch({ + http: core.http, emails, schedule, serviceName, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c7860b81a7b1e..f05d343ad7ba5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -7,22 +7,30 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; import uuid from 'uuid'; -// @ts-ignore import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; +import { HttpServiceBase } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; +jest.mock('../../../../../services/rest/callApi', () => ({ + callApi: () => Promise.resolve(null) +})); + describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; + const createWatchSpy = jest + .spyOn(rest, 'createWatch') + .mockResolvedValue(undefined); + beforeEach(async () => { jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); - jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); createWatchResponse = await createErrorGroupWatch({ + http: {} as HttpServiceBase, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { @@ -36,19 +44,19 @@ describe('createErrorGroupWatch', () => { apmIndexPatternTitle: 'myIndexPattern' }); - const watchBody = rest.createWatch.mock.calls[0][1]; + const watchBody = createWatchSpy.mock.calls[0][0].watch; const templateCtx = { payload: esResponse, metadata: watchBody.metadata }; - tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx); + tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); }); afterEach(() => jest.restoreAllMocks()); it('should call createWatch with correct args', () => { - expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid'); + expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); }); it('should format slack message correctly', () => { @@ -78,7 +86,7 @@ describe('createErrorGroupWatch', () => { }); it('should return watch id', async () => { - const id = rest.createWatch.mock.calls[0][0]; + const id = createWatchSpy.mock.calls[0][0].id; expect(createWatchResponse).toEqual(id); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index e7d06403b8f8e..1d21e35f122d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; +import { HttpServiceBase } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -17,7 +18,6 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; -// @ts-ignore import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { @@ -35,6 +35,7 @@ export interface Schedule { } interface Arguments { + http: HttpServiceBase; emails: string[]; schedule: Schedule; serviceName: string; @@ -54,6 +55,7 @@ interface Actions { } export async function createErrorGroupWatch({ + http, emails = [], schedule, serviceName, @@ -250,6 +252,10 @@ export async function createErrorGroupWatch({ }; } - await createWatch(id, body); + await createWatch({ + http, + id, + watch: body + }); return id; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 146f6f58031bb..904e16f92f4a0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -7,11 +7,11 @@ exports[`ServiceOverview -> List should render columns correctly 1`] = ` id="service-name-tooltip" position="top" > - opbeans-python - + `; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 6dcb9140a8bb9..489d4f2908cbe 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -138,7 +138,15 @@ NodeList [ exports[`Service Overview -> View should render services, when list is not empty 1`] = ` NodeList [ - @@ -236,7 +244,15 @@ NodeList [ , - diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 67957ae76b1f1..6323599436ca8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -63,13 +63,6 @@ const APM_INDEX_LABELS = [ label: i18n.translate('xpack.apm.settings.apmIndices.metricsIndicesLabel', { defaultMessage: 'Metrics Indices' }) - }, - { - configurationName: 'apm_oss.apmAgentConfigurationIndex', - label: i18n.translate( - 'xpack.apm.settings.apmIndices.apmAgentConfigurationIndexLabel', - { defaultMessage: 'Agent Configuration Index' } - ) } ]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index cd892b370219a..1a3f1f6831ff3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -33,7 +33,7 @@ registerLanguage('sql', sql); const DatabaseStatement = styled.div` padding: ${px(units.half)} ${px(unit)}; - background: ${tint(0.1, theme.euiColorWarning)} + background: ${tint(0.1, theme.euiColorWarning)}; border-radius: ${borderRadius}; border: 1px solid ${theme.euiColorLightShade}; font-family: ${fontFamilyCode}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx index b01dc2c5398e2..a42b2435ff914 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx @@ -19,7 +19,7 @@ interface Props { previewHeight: number; } -export const TruncateHeightSection: React.SFC = ({ +export const TruncateHeightSection: React.FC = ({ children, previewHeight }) => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index c64231a6ded86..40dc60c1efacd 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -27,12 +27,10 @@ interface IContainerStyleProps { interface IBarStyleProps { type: ItemType; - left: number; - width: number; color: string; } -const Container = styled('div')` +const Container = styled.div` position: relative; display: block; user-select: none; @@ -50,7 +48,7 @@ const Container = styled('div')` } `; -const ItemBar = styled('div')` +const ItemBar = styled.div` box-sizing: border-box; position: relative; height: ${px(unit)}; @@ -114,7 +112,7 @@ interface SpanActionToolTipProps { item?: IWaterfallItem; } -const SpanActionToolTip: React.SFC = ({ +const SpanActionToolTip: React.FC = ({ item, children }) => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index e518751e4153e..fb37bf44c2f32 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -110,7 +110,7 @@ interface Props { isLoading: boolean; } -export const WaterfallWithSummmary: React.SFC = ({ +export const WaterfallWithSummmary: React.FC = ({ urlParams, location, waterfall, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 881e5975fc81f..05094c59712a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -18,8 +18,6 @@ import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; -jest.mock('ui/kfetch'); - const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); const MockUrlParamsProvider: React.FC<{ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx index f18d12c4f6edc..3c279d525410c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -14,7 +14,7 @@ interface Props { hideSubheading?: boolean; } -const EmptyMessage: React.SFC = ({ +const EmptyMessage: React.FC = ({ heading = i18n.translate('xpack.apm.emptyMessage.noDataFoundLabel', { defaultMessage: 'No data found.' }), diff --git a/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx index e6f4487312429..aeb2d25ffeae1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useRef } from 'react'; -export const HeightRetainer: React.SFC = props => { +export const HeightRetainer: React.FC = props => { const containerElement = useRef(null); const minHeight = useRef(0); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 66946e5b447f9..32fbe46ac560c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,9 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; -import { StaticIndexPattern } from 'ui/index_patterns'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; // @ts-ignore @@ -18,36 +15,36 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { usePlugins } from '../../../new-platform/plugin'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; +import { + AutocompleteProvider, + AutocompleteSuggestion, + esKuery, + IIndexPattern +} from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; `; -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - interface State { suggestions: AutocompleteSuggestion[]; isLoadingSuggestions: boolean; } -function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( query: string, selectionStart: number, - indexPattern: StaticIndexPattern, - boolFilter: unknown + indexPattern: IIndexPattern, + boolFilter: unknown, + autocompleteProvider?: AutocompleteProvider ) { - const autocompleteProvider = getAutocompleteProvider('kuery'); if (!autocompleteProvider) { return []; } @@ -74,6 +71,8 @@ export function KueryBar() { }); const { urlParams } = useUrlParams(); const location = useLocation(); + const { data } = usePlugins(); + const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -108,7 +107,8 @@ export function KueryBar() { inputValue, selectionStart, indexPattern, - boolFilter + boolFilter, + autocompleteProvider ) ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx index 827b824e5d05c..38f7046685636 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx @@ -32,7 +32,7 @@ function getDiscoverQuery(error: APMError, kuery?: string) { }; } -const DiscoverErrorLink: React.SFC<{ +const DiscoverErrorLink: React.FC<{ readonly error: APMError; readonly kuery?: string; }> = ({ error, kuery, children }) => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx index 48dd4e5b57a43..374454b74fce4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx @@ -22,7 +22,7 @@ function getDiscoverQuery(span: Span) { }; } -export const DiscoverSpanLink: React.SFC<{ +export const DiscoverSpanLink: React.FC<{ readonly span: Span; }> = ({ span, children }) => { return ; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx index df86008c193ff..0e381c490f8b4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx @@ -32,7 +32,7 @@ export function getDiscoverQuery(transaction: Transaction) { }; } -export const DiscoverTransactionLink: React.SFC<{ +export const DiscoverTransactionLink: React.FC<{ readonly transaction: Transaction; }> = ({ transaction, children }) => { return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx index 26f830a3b8efb..33cf05401a729 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx @@ -5,7 +5,6 @@ */ import { shallow, ShallowWrapper } from 'enzyme'; -import 'jest-styled-components'; import React from 'react'; import { APMError } from '../../../../../../typings/es_schemas/ui/APMError'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx index 26f830a3b8efb..33cf05401a729 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx @@ -5,7 +5,6 @@ */ import { shallow, ShallowWrapper } from 'enzyme'; -import 'jest-styled-components'; import React from 'react'; import { APMError } from '../../../../../../typings/es_schemas/ui/APMError'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx index 979391f260eda..a16bf389f177c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx @@ -5,7 +5,6 @@ */ import { shallow } from 'enzyme'; -import 'jest-styled-components'; import React from 'react'; import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; import { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx index 9b314615f6413..3b876aa5950b4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'jest-styled-components'; import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; // @ts-ignore import configureStore from '../../../../../store/config/configureStore'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index 1cf10d13e57bb..81c5d17d491c0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -13,7 +13,7 @@ interface Props { transactionType?: string; } -export const MLJobLink: React.SFC = ({ +export const MLJobLink: React.FC = ({ serviceName, transactionType, children diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx index a257e52ab01cc..d2e9578b45e07 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -const Button = styled(EuiButtonEmpty).attrs({ +const Button = styled(EuiButtonEmpty).attrs(() => ({ contentProps: { className: 'alignLeft' }, color: 'text' -})` +}))` width: 100%; .alignLeft { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx index 412b92d525aa2..f47bc92567f9f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { FunctionComponent, useState, useMemo, useEffect } from 'react'; import { EuiTitle, EuiPopover, + EuiPopoverProps, EuiSelectable, EuiSpacer, EuiHorizontalRule, @@ -23,9 +24,11 @@ import { FilterBadgeList } from './FilterBadgeList'; import { unit, px } from '../../../../style/variables'; import { FilterTitleButton } from './FilterTitleButton'; -const Popover = styled(EuiPopover).attrs({ - anchorClassName: 'anchor' -})` +const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( + () => ({ + anchorClassName: 'anchor' + }) +)` .anchor { display: block; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index 5ec6a2289f9c9..84c2801a45049 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -30,7 +30,7 @@ interface Props { isLibraryFrame: boolean; } -const FrameHeading: React.SFC = ({ stackframe, isLibraryFrame }) => { +const FrameHeading: React.FC = ({ stackframe, isLibraryFrame }) => { const FileDetail = isLibraryFrame ? LibraryFrameFileDetail : AppFrameFileDetail; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx index 5f6519b7c7b96..b485b4f844f64 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx @@ -5,7 +5,6 @@ */ import { mount, ReactWrapper, shallow } from 'enzyme'; -import 'jest-styled-components'; import React from 'react'; import { IStackframe } from '../../../../../typings/es_schemas/raw/fields/Stackframe'; import { Stackframe } from '../Stackframe'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap index 55cec7389dc5c..5b17b124a321d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap @@ -394,8 +394,72 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] className="c8" > + url.full - + } delay="regular" position="top" @@ -59,9 +59,9 @@ exports[`StickyProperties should render entire component 1`] = ` + http.request.method - + } delay="regular" position="top" @@ -88,9 +88,9 @@ exports[`StickyProperties should render entire component 1`] = ` + error.exception.handled - + } delay="regular" position="top" @@ -117,9 +117,9 @@ exports[`StickyProperties should render entire component 1`] = ` + user.id - + } delay="regular" position="top" diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index aeef8ed995e92..ff917dd95bf96 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -6,28 +6,28 @@ Array [ "color": "#3185fc", "disabled": undefined, "onClick": [Function], - "text": + "text": Avg. - + 468 ms - - , + + , }, Object { "color": "#e6c220", "disabled": undefined, "onClick": [Function], - "text": + "text": 95th percentile - , + , }, Object { "color": "#f98510", "disabled": undefined, "onClick": [Function], - "text": + "text": 99th percentile - , + , }, ] `; @@ -2703,66 +2703,112 @@ Array [ display: inline-block; } -
+
- - - Avg. - - 468 ms - - -
-
- - + + + + + Avg. + + + 468 ms + + + + +
+
+ - 95th percentile - -
-
- - + + + + + + 95th percentile + + +
+ + - 99th percentile - +
+ + + + + + 99th percentile + + +
+
- , + , ] `; @@ -5077,85 +5123,159 @@ Array [ } } > -
+
- 1502283720 -
-
-
-
- - Avg. -
-
- 438704.4 -
-
-
-
- - 95th -
+
- 1557383.999999999 + 1502283720
-
-
+ +
- - 99th + +
+ + +
+ + + + Avg. +
+
+
+ +
+ 438704.4 +
+
+
+
+ +
+ + +
+ + + + 95th +
+
+
+ +
+ 1557383.999999999 +
+
+
+
+ +
+ + +
+ + + + 99th +
+
+
+ +
+ 1820377.1200000006 +
+
+
+
+
+
- 1820377.1200000006 -
-
+ className="c10" + /> +
-
-
+
@@ -5740,66 +5860,112 @@ Array [ display: inline-block; } -
+
- - - Avg. - - 468 ms - - -
-
- - + + + + + Avg. + + + 468 ms + + + + +
+
+ - 95th percentile - -
-
- - + + + + + + 95th percentile + + +
+ + - 99th percentile - +
+ + + + + + 99th percentile + + +
+
- , + , ] `; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 468116b84b6bb..da71e264ac099 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -19,1400 +19,1402 @@ exports[`Histogram Initially should have default markup 1`] = ` } } > -
+
- - - - - - - + - + + + + + - - + - - - 0 ms - - - - - + + 0 ms + + + - 500 ms - - - - - + + 500 ms + + + - 1,000 ms - - - - - + + 1,000 ms + + + - 1,500 ms - - - - - + + 1,500 ms + + + - 2,000 ms - - - - - + + 2,000 ms + + + - 2,500 ms - - - - - + + 2,500 ms + + + - 3,000 ms - + + + 3,000 ms + + - - - - - 0.0 occ. - - - - - + + 0.0 occ. + + + - 27.5 occ. - - - - - + + 27.5 occ. + + + - 55.0 occ. - + + + 55.0 occ. + + - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + /> + + +
- + `; @@ -1467,26 +1469,36 @@ exports[`Histogram when hovering over a non-empty bucket should have correct mar } } > -
-
- 811 - 927 ms -
+
-
- 49.0 occurrences -
+ +
+ 811 - 927 ms +
+
+ +
+ +
+ 49.0 occurrences +
+
+
+
+ +
+
-
-
+
`; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap index c1bde70f93429..80baba28fcfdf 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap @@ -540,19 +540,32 @@ exports[`Timeline should render with data 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} > -
- -
+
+ + + +
+
-
- -
+
+ + + +
+
-
- -
+
+ + + +
+
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx new file mode 100644 index 0000000000000..e95f733fb4bc8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { BrowserLineChart } from './BrowserLineChart'; + +describe('BrowserLineChart', () => { + describe('render', () => { + it('renders', () => { + expect(() => shallow()).not.toThrowError(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx new file mode 100644 index 0000000000000..58bc4655f730c --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTitle } from '@elastic/eui'; +import { TransactionLineChart } from './TransactionLineChart'; +import { + getMaxY, + getResponseTimeTickFormatter, + getResponseTimeTooltipFormatter +} from '.'; +import { getDurationFormatter } from '../../../../utils/formatters'; +import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; + +export function BrowserLineChart() { + const { data } = useAvgDurationByBrowser(); + const maxY = getMaxY(data); + const formatter = getDurationFormatter(maxY); + const formatTooltipValue = getResponseTimeTooltipFormatter(formatter); + const tickFormatY = getResponseTimeTickFormatter(formatter); + + return ( + <> + + + {i18n.translate( + 'xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser', + { + defaultMessage: 'Avg. page load duration distribution by browser' + } + )} + + + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index d2b6970841bdc..6db91b4368aa9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; -export const ChoroplethToolTip: React.SFC<{ +export const ChoroplethToolTip: React.FC<{ name: string; value: number; docCount: number; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx index bb4974d6d51fa..e98b695c14763 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx @@ -65,7 +65,7 @@ const getMin = (items: ChoroplethItem[]) => const getMax = (items: ChoroplethItem[]) => Math.max(...items.map(item => item.value)); -export const ChoroplethMap: React.SFC = props => { +export const ChoroplethMap: React.FC = props => { const { items } = props; const containerRef = useRef(null); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx index 6176397170797..0761cec53fc2e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCountry'; + import { ChoroplethMap } from '../ChoroplethMap'; -export const DurationByCountryMap: React.SFC = () => { +export const DurationByCountryMap: React.FC = () => { const { data } = useAvgDurationByCountry(); return ( - - - - - - {i18n.translate( - 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel', - { - defaultMessage: - 'Avg. page load duration distribution by country' - } - )} - - - - - - + <> + {' '} + + + {i18n.translate( + 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel', + { + defaultMessage: 'Avg. page load duration distribution by country' + } + )} + + + + ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index c032d60359903..97794bf66687b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -33,6 +33,7 @@ import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { BrowserLineChart } from './BrowserLineChart'; import { DurationByCountryMap } from './DurationByCountryMap'; import { TRANSACTION_PAGE_LOAD, @@ -59,31 +60,29 @@ const ShiftedEuiText = styled(EuiText)` top: 5px; `; -export class TransactionCharts extends Component { - public getMaxY = (responseTimeSeries: TimeSeries[]) => { - const coordinates = flatten( - responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) - ); - - const numbers: number[] = coordinates.map((c: Coordinate) => - c.y ? c.y : 0 - ); +export function getResponseTimeTickFormatter(formatter: TimeFormatter) { + return (t: number) => formatter(t).formatted; +} - return Math.max(...numbers, 0); +export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { + return (p: Coordinate) => { + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; }; +} - public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { - return (t: number) => formatter(t).formatted; - }; +export function getMaxY(responseTimeSeries: TimeSeries[]) { + const coordinates = flatten( + responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); - public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { - return (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? formatter(p.y).formatted - : NOT_AVAILABLE_LABEL; - }; - }; + const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); + return Math.max(...numbers, 0); +} + +export class TransactionCharts extends Component { public getTPMFormatter = (t: number) => { const { urlParams } = this.props; const unit = tpmUnit(urlParams.transactionType); @@ -154,7 +153,7 @@ export class TransactionCharts extends Component { const { charts, urlParams } = this.props; const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; - const maxY = this.getMaxY(responseTimeSeries); + const maxY = getMaxY(responseTimeSeries); const formatter = getDurationFormatter(maxY); return ( @@ -177,8 +176,8 @@ export class TransactionCharts extends Component { @@ -205,7 +204,18 @@ export class TransactionCharts extends Component { {transactionType === TRANSACTION_PAGE_LOAD && ( <> - + + + + + + + + + + + + )} diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts index 5fa9294a95dfd..1806e7395a8cc 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -83,24 +83,24 @@ export function getPathParams(pathname: string = ''): PathParams { switch (servicePageName) { case 'transactions': return { - processorEvent: 'transaction', + processorEvent: ProcessorEvent.transaction, serviceName }; case 'errors': return { - processorEvent: 'error', + processorEvent: ProcessorEvent.error, serviceName, errorGroupId: paths[3] }; case 'metrics': return { - processorEvent: 'metric', + processorEvent: ProcessorEvent.metric, serviceName, serviceNodeName }; case 'nodes': return { - processorEvent: 'metric', + processorEvent: ProcessorEvent.metric, serviceName }; case 'service-map': @@ -113,7 +113,7 @@ export function getPathParams(pathname: string = ''): PathParams { case 'traces': return { - processorEvent: 'transaction' + processorEvent: ProcessorEvent.transaction }; default: return {}; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts new file mode 100644 index 0000000000000..38f26c2ba9fbd --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from 'react-hooks-testing-library'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import * as useFetcherModule from './useFetcher'; +import { useAvgDurationByBrowser } from './useAvgDurationByBrowser'; + +describe('useAvgDurationByBrowser', () => { + it('returns data', () => { + const data = [ + { title: 'Other', data: [{ x: 1572530100000, y: 130010.8947368421 }] } + ]; + jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ + data, + refetch: () => {}, + status: 'success' as useFetcherModule.FETCH_STATUS + }); + const { result } = renderHook(() => useAvgDurationByBrowser()); + + expect(result.current.data).toEqual([ + { + color: theme.euiColorVis0, + data: [{ x: 1572530100000, y: 130010.8947368421 }], + title: 'Other', + type: 'linemark' + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts new file mode 100644 index 0000000000000..a1e9294455d54 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 theme from '@elastic/eui/dist/eui_theme_light.json'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; +import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; +import { TimeSeries } from '../../typings/timeseries'; +import { getVizColorForIndex } from '../../common/viz_colors'; + +function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { + if (!data) { + return []; + } + + return data.map((item, index) => { + return { + ...item, + color: getVizColorForIndex(index, theme), + type: 'linemark' + }; + }); +} + +export function useAvgDurationByBrowser() { + const { + urlParams: { serviceName, start, end, transactionName }, + uiFilters + } = useUrlParams(); + + const { data, error, status } = useFetcher( + callApmApi => { + if (serviceName && start && end) { + return callApmApi({ + pathname: + '/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser', + params: { + path: { serviceName }, + query: { + start, + end, + transactionName, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, start, end, transactionName, uiFilters] + ); + + return { + data: toTimeSeries(data), + status, + error + }; +} diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index 7d6fa70f025aa..db14e1c520020 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -4,35 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; import { npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; +import { PluginInitializerContext } from 'kibana/public'; import 'ui/autoload/all'; import chrome from 'ui/chrome'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { GlobalHelpExtension } from './components/app/GlobalHelpExtension'; import { plugin } from './new-platform'; import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; -import { KibanaCoreContextProvider } from '../../observability/public'; -const { core } = npStart; -// render APM feedback link in global help menu -core.chrome.setHelpExtension(domElement => { - ReactDOM.render( - - - , - domElement - ); - return () => { - ReactDOM.unmountComponentAtNode(domElement); - }; -}); +const { core, plugins } = npStart; +// This will be moved to core.application.register when the new platform +// migration is complete. // @ts-ignore chrome.setRootTemplate(template); @@ -47,5 +32,5 @@ const checkForRoot = () => { }); }; checkForRoot().then(() => { - plugin().start(core); + plugin({} as PluginInitializerContext).start(core, plugins); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx index cb4cc2a845a4c..9dce4bcdd828c 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { PluginInitializer } from '../../../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; -export function plugin() { - return new Plugin(); -} +export const plugin: PluginInitializer< + ApmPluginSetup, + ApmPluginStart +> = _core => new ApmPlugin(); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index 637dcbcd52f58..b5986610d3048 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext, createContext } from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { LegacyCoreStart } from 'src/core/public'; +import { + CoreStart, + LegacyCoreStart, + Plugin, + CoreSetup +} from '../../../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { KibanaCoreContextProvider } from '../../../observability/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; @@ -19,9 +25,10 @@ import { LicenseProvider } from '../context/LicenseContext'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; +import { setHelpExtension } from './setHelpExtension'; +import { setReadonlyBadge } from './updateBadge'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -31,46 +38,80 @@ const MainContainer = styled.main` `; const App = () => { - useUpdateBadgeEffect(); - return ( - - - - - - - - - {routes.map((route, i) => ( - - ))} - - - - - - + + + + + {routes.map((route, i) => ( + + ))} + + ); }; -export class Plugin { - public start(core: LegacyCoreStart) { - const { i18n } = core; +export type ApmPluginSetup = void; +export type ApmPluginStart = void; +export type ApmPluginSetupDeps = {}; // eslint-disable-line @typescript-eslint/consistent-type-definitions + +export interface ApmPluginStartDeps { + data: DataPublicPluginStart; +} + +const PluginsContext = createContext({} as ApmPluginStartDeps); + +export function usePlugins() { + return useContext(PluginsContext); +} + +export class ApmPlugin + implements + Plugin< + ApmPluginSetup, + ApmPluginStart, + ApmPluginSetupDeps, + ApmPluginStartDeps + > { + // Take the DOM element as the constructor, so we can mount the app. + public setup(_core: CoreSetup, _plugins: ApmPluginSetupDeps) {} + + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + const i18nCore = core.i18n; + + // render APM feedback link in global help menu + setHelpExtension(core); + setReadonlyBadge(core); + ReactDOM.render( - - - - - - - - + + + + + + + + + + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); // create static index pattern and store as saved object. Not needed by APM UI but for legacy reasons in Discover, Dashboard etc. - createStaticIndexPattern(core.http); + createStaticIndexPattern(core.http).catch(e => { + // eslint-disable-next-line no-console + console.log('Error fetching static index pattern', e); + }); } + + public stop() {} } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts new file mode 100644 index 0000000000000..1a3394651b2ff --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import url from 'url'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setHelpExtension({ chrome, http }: CoreStart) { + chrome.setHelpExtension({ + appName: i18n.translate('xpack.apm.feedbackMenu.appName', { + defaultMessage: 'APM' + }), + links: [ + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/apm' + }, + { + linkType: 'custom', + href: url.format({ + pathname: http.basePath.prepend('/app/kibana'), + hash: '/management/elasticsearch/upgrade_assistant' + }), + content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { + defaultMessage: 'Upgrade assistant' + }) + } + ] + }); +} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts new file mode 100644 index 0000000000000..b3e29bb891c23 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setReadonlyBadge({ application, chrome }: CoreStart) { + const canSave = application.capabilities.apm.save; + const { setBadge } = chrome; + + setBadge( + !canSave + ? { + text: i18n.translate('xpack.apm.header.badge.readOnly.text', { + defaultMessage: 'Read only' + }), + tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save' + }), + iconType: 'glasses' + } + : undefined + ); +} diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts index cd681e354e2ed..31ba1e8d40aaa 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts @@ -48,7 +48,7 @@ describe('callApi', () => { it('should not add debug param for non-APM endpoints', async () => { await callApi(http, { pathname: `/api/kibana` }); - expect(http.get).toHaveBeenCalledWith('/api/kibana', {}); + expect(http.get).toHaveBeenCalledWith('/api/kibana', { query: {} }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts index 22b37c275596f..e8a9fa74bd1da 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts @@ -81,10 +81,10 @@ describe('callApmApi', () => { expect.objectContaining({ pathname: '/api/apm', method: 'POST', - body: JSON.stringify({ + body: { foo: 'bar', bar: 'foo' - }) + } }) ); }); diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index 031f21e6e2feb..887200bdfc22a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -9,10 +9,11 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; -export type FetchOptions = HttpFetchOptions & { +export type FetchOptions = Omit & { pathname: string; forceCache?: boolean; method?: string; + body?: any; }; function fetchOptionsWithDebug(fetchOptions: FetchOptions) { @@ -20,15 +21,21 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { sessionStorage.getItem('apm_debug') === 'true' && startsWith(fetchOptions.pathname, '/api/apm'); - if (!debugEnabled) { - return fetchOptions; - } + const isGet = !fetchOptions.method || fetchOptions.method === 'GET'; + + // Need an empty body to pass route validation + const body = isGet + ? {} + : { + body: JSON.stringify(fetchOptions.body || {}) + }; return { ...fetchOptions, + ...body, query: { ...fetchOptions.query, - _debug: true + ...(debugEnabled ? { _debug: true } : {}) } }; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts index e2084599a0499..964cc12794075 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts @@ -22,10 +22,6 @@ export const createCallApmApi = (http: HttpServiceBase) => const { pathname, params = {}, ...opts } = options; const path = (params.path || {}) as Record; - const body = params.body - ? { body: JSON.stringify(params.body) } - : undefined; - const query = params.query ? { query: params.query } : undefined; const formattedPathname = Object.keys(path).reduce((acc, paramName) => { return acc.replace(`{${paramName}}`, path[paramName]); @@ -34,7 +30,7 @@ export const createCallApmApi = (http: HttpServiceBase) => return callApi(http, { ...opts, pathname: formattedPathname, - ...body, - ...query + body: params.body, + query: params.query }); }) as APMClient; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js b/x-pack/legacy/plugins/apm/public/services/rest/watcher.js deleted file mode 100644 index 9d68a1665912c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js +++ /dev/null @@ -1,15 +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 { callApi } from './callApi'; - -export async function createWatch(id, watch) { - return callApi({ - method: 'PUT', - pathname: `/api/watcher/watch/${id}`, - body: JSON.stringify({ type: 'json', id, watch }) - }); -} diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts new file mode 100644 index 0000000000000..dfa64b5368ee9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpServiceBase } from 'kibana/public'; +import { callApi } from './callApi'; + +export async function createWatch({ + id, + watch, + http +}: { + http: HttpServiceBase; + id: string; + watch: any; +}) { + return callApi(http, { + method: 'PUT', + pathname: `/api/watcher/watch/${id}`, + body: { type: 'json', id, watch } + }); +} diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index 60a5f7172b956..321ce761422f0 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -9,12 +9,12 @@ import { ReactWrapper } from 'enzyme'; import enzymeToJson from 'enzyme-to-json'; import { Location } from 'history'; -import 'jest-styled-components'; import moment from 'moment'; import { Moment } from 'moment-timezone'; import React from 'react'; import { render, waitForElement } from 'react-testing-library'; import { MemoryRouter } from 'react-router-dom'; +import { APMConfig } from '../../../../../plugins/apm/server'; import { LocationProvider } from '../context/LocationContext'; import { PromiseReturnType } from '../../typings/common'; import { ESFilter } from '../../typings/elasticsearch'; @@ -99,10 +99,7 @@ interface MockSetup { end: number; client: any; internalClient: any; - config: { - get: any; - has: any; - }; + config: APMConfig; uiFiltersES: ESFilter[]; indices: { 'apm_oss.sourcemapIndices': string; @@ -111,7 +108,7 @@ interface MockSetup { 'apm_oss.spanIndices': string; 'apm_oss.transactionIndices': string; 'apm_oss.metricsIndices': string; - 'apm_oss.apmAgentConfigurationIndex': string; + apmAgentConfigurationIndex: string; }; } @@ -139,10 +136,12 @@ export async function inspectSearchParams( internalClient: { search: internalClientSpy } as any, - config: { - get: () => 'myIndex' as any, - has: () => true - }, + config: new Proxy( + {}, + { + get: () => 'myIndex' + } + ) as APMConfig, uiFiltersES: [ { term: { 'service.environment': 'prod' } @@ -155,7 +154,7 @@ export async function inspectSearchParams( 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - 'apm_oss.apmAgentConfigurationIndex': 'myIndex' + apmAgentConfigurationIndex: 'myIndex' }, dynamicIndexPattern: null as any }; diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index a46b0c2895fca..c5fc1be2b4d56 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -8,7 +8,7 @@ git clone git@github.com:elastic/kibana.git cd kibana/ yarn kbn bootstrap -yarn start +yarn start --no-base-path ``` #### APM Server, Elasticsearch and data diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json index e7d9abea65a3a..c2f87503b4548 100644 --- a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json +++ b/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../../apm.tsconfig.json", "include": [ "./**/*", + "../../../plugins/apm/**/*", "../../../typings/**/*" ], "exclude": [ diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts index 640072d6ec4d8..ddfb4144d9636 100644 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { countBy } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; -import { CoreSetup } from 'src/core/server'; import { isAgentName } from '../../../common/agent_name'; import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; import { APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID } from '../../../common/apm_saved_object_constants'; -import { LegacySetup } from '../../new-platform/plugin'; +import { APMLegacyServer } from '../../routes/typings'; +import { UsageCollectionSetup } from '../../../../../../../src/plugins/usage_collection/server'; export function createApmTelementry( agentNames: string[] = [] @@ -27,12 +26,12 @@ export function createApmTelementry( } export async function storeApmServicesTelemetry( - server: Server, + server: APMLegacyServer, apmTelemetry: SavedObjectAttributes ) { try { - const internalSavedObjectsClient = getInternalSavedObjectsClient(server); - await internalSavedObjectsClient.create( + const savedObjectsClient = getInternalSavedObjectsClient(server); + await savedObjectsClient.create( APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, apmTelemetry, { @@ -45,22 +44,11 @@ export async function storeApmServicesTelemetry( } } -interface LegacySetupWithUsageCollector extends LegacySetup { - server: LegacySetup['server'] & { - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - }; -} - export function makeApmUsageCollector( - core: CoreSetup, - { server }: LegacySetupWithUsageCollector + usageCollector: UsageCollectionSetup, + server: APMLegacyServer ) { - const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ + const apmUsageCollector = usageCollector.makeUsageCollector({ type: 'apm', fetch: async () => { const internalSavedObjectsClient = getInternalSavedObjectsClient(server); @@ -76,5 +64,6 @@ export function makeApmUsageCollector( }, isReady: () => true }); - server.usage.collectorSet.register(apmUsageCollector); + + usageCollector.registerCollector(apmUsageCollector); } diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index e1d7e9843bf69..0065c28f60d2d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -11,7 +11,7 @@ Object { "min": 1528113600000, }, "field": "@timestamp", - "interval": NaN, + "interval": 57600000, "min_doc_count": 0, }, }, @@ -63,7 +63,7 @@ Object { "min": 1528113600000, }, "field": "@timestamp", - "interval": NaN, + "interval": 57600000, "min_doc_count": 0, }, }, diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 18ffb743934c0..cf8798d445f8a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -6,6 +6,7 @@ import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; +import { APMConfig } from '../../../../../../../../plugins/apm/server'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -34,10 +35,12 @@ describe('timeseriesFetcher', () => { internalClient: { search: clientSpy } as any, - config: { - get: () => 'myIndex' as any, - has: () => true - }, + config: new Proxy( + {}, + { + get: () => 'myIndex' + } + ) as APMConfig, uiFiltersES: [ { term: { 'service.environment': 'prod' } @@ -50,7 +53,7 @@ describe('timeseriesFetcher', () => { 'apm_oss.spanIndices': 'apm-*', 'apm_oss.transactionIndices': 'apm-*', 'apm_oss.metricsIndices': 'apm-*', - 'apm_oss.apmAgentConfigurationIndex': '.apm-agent-configuration' + apmAgentConfigurationIndex: '.apm-agent-configuration' }, dynamicIndexPattern: null as any } diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 37889e69ad8f2..9274f96d58d83 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -11,7 +11,11 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../helpers/range_filter'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; export async function getBuckets({ serviceName, @@ -22,7 +26,7 @@ export async function getBuckets({ serviceName: string; groupId?: string; bucketSize: number; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, client, indices } = setup; const filter: ESFilter[] = [ diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts index 81019b5261044..6172c71a0ed15 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -5,12 +5,16 @@ */ import { PromiseReturnType } from '../../../../typings/common'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { getBuckets } from './get_buckets'; +import { BUCKET_TARGET_COUNT } from '../../transactions/constants'; -function getBucketSize({ start, end, config }: Setup) { - const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); - return Math.floor((end - start) / bucketTargetCount); +function getBucketSize({ start, end }: SetupTimeRange) { + return Math.floor((end - start) / BUCKET_TARGET_COUNT); } export type ErrorDistributionAPIResponse = PromiseReturnType< @@ -24,9 +28,9 @@ export async function getErrorDistribution({ }: { serviceName: string; groupId?: string; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const bucketSize = getBucketSize(setup); + const bucketSize = getBucketSize({ start: setup.start, end: setup.end }); const { buckets, noHits } = await getBuckets({ serviceName, groupId, diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts index fd1199d07b95f..8d19455651be3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts @@ -13,7 +13,11 @@ import { import { PromiseReturnType } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; export type ErrorGroupAPIResponse = PromiseReturnType; @@ -26,7 +30,7 @@ export async function getErrorGroup({ }: { serviceName: string; groupId: string; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts index 24e7114efddee..aaa4ca9fb8223 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,7 +13,11 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getErrorGroupsProjection } from '../../../common/projections/errors'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; @@ -31,7 +35,7 @@ export async function getErrorGroups({ serviceName: string; sortField?: string; sortDirection?: 'asc' | 'desc'; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { client } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts index 5074f9315d8ae..acbbeed2a527d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts @@ -11,7 +11,7 @@ import { TRANSACTION_ID } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export interface ErrorsPerTransaction { [transactionId: string]: number; @@ -21,7 +21,7 @@ const includedLogLevels = ['critical', 'error', 'fatal']; export async function getTraceErrorsPerTransaction( traceId: string, - setup: Setup + setup: Setup & SetupTimeRange ): Promise { const { start, end, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index b9ca268c86beb..a6f6d36ecfc81 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query'; import { ESFilter } from '../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; @@ -12,14 +11,16 @@ import { localUIFilters, localUIFilterNames } from '../../ui_filters/local_ui_filters/config'; -import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esKuery, + IIndexPattern +} from '../../../../../../../../src/plugins/data/server'; export function getUiFiltersES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFilters: UIFilters ) { const { kuery, environment, ...localFilterValues } = uiFilters; - const mappedFilters = localUIFilterNames .filter(name => name in localFilterValues) .map(filterName => { @@ -44,13 +45,13 @@ export function getUiFiltersES( } function getKueryUiFilterES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, kuery?: string ) { if (!kuery || !indexPattern) { return; } - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern) as ESFilter; + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter; } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 9c111910f16f9..28035ac2f9be2 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -11,14 +11,17 @@ import { IndicesDeleteParams, IndicesCreateParams } from 'elasticsearch'; -import { Legacy } from 'kibana'; -import { cloneDeep, has, isString, set, pick } from 'lodash'; +import { merge } from 'lodash'; +import { cloneDeep, isString } from 'lodash'; +import { KibanaRequest } from 'src/core/server'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { ESSearchResponse, ESSearchRequest } from '../../../typings/elasticsearch'; +import { APMRequestHandlerContext } from '../../routes/typings'; +import { pickKeys } from '../../../public/utils/pickKeys'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; // `type` was deprecated in 7.0 export type APMIndexDocumentParams = Omit, 'type'>; @@ -38,7 +41,7 @@ export function isApmIndex( function addFilterForLegacyData( apmIndices: string[], - params: SearchParams, + params: ESSearchRequest, { includeLegacyData = false } = {} ): SearchParams { // search across all data (including data) @@ -46,10 +49,18 @@ function addFilterForLegacyData( return params; } - const nextParams = cloneDeep(params); - if (!has(nextParams, 'body.query.bool.filter')) { - set(nextParams, 'body.query.bool.filter', []); - } + const nextParams = merge( + { + body: { + query: { + bool: { + filter: [] + } + } + } + }, + cloneDeep(params) + ); // add filter for omitting pre-7.x data nextParams.body.query.bool.filter.push({ @@ -61,30 +72,27 @@ function addFilterForLegacyData( // add additional params for search (aka: read) requests async function getParamsForSearchRequest( - req: Legacy.Request, - params: SearchParams, + context: APMRequestHandlerContext, + params: ESSearchRequest, apmOptions?: APMOptions ) { - const uiSettings = req.getUiSettingsService(); - const { server } = req; + const { uiSettings } = context.core; const [indices, includeFrozen] = await Promise.all([ - getApmIndices({ - config: server.config(), - savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(req) - }), - uiSettings.get('search:includeFrozen') + getApmIndices(context), + uiSettings.client.get('search:includeFrozen') ]); // Get indices for legacy data filter (only those which apply) - const apmIndices: string[] = Object.values( - pick(indices, [ + const apmIndices = Object.values( + pickKeys( + indices, 'apm_oss.sourcemapIndices', 'apm_oss.errorIndices', 'apm_oss.onboardingIndices', 'apm_oss.spanIndices', 'apm_oss.transactionIndices', 'apm_oss.metricsIndices' - ]) + ) ); return { ...addFilterForLegacyData(apmIndices, params, apmOptions), // filter out pre-7.0 data @@ -103,15 +111,18 @@ interface ClientCreateOptions { export type ESClient = ReturnType; export function getESClient( - req: Legacy.Request, + context: APMRequestHandlerContext, + request: KibanaRequest, { clientAsInternalUser = false }: ClientCreateOptions = {} ) { - const cluster = req.server.plugins.elasticsearch.getCluster('data'); - const query = req.query as Record; + const { + callAsCurrentUser, + callAsInternalUser + } = context.core.elasticsearch.dataClient; const callMethod = clientAsInternalUser - ? cluster.callWithInternalUser.bind(cluster) - : cluster.callWithRequest.bind(cluster, req); + ? callAsInternalUser + : callAsCurrentUser; return { search: async < @@ -122,17 +133,15 @@ export function getESClient( apmOptions?: APMOptions ): Promise> => { const nextParams = await getParamsForSearchRequest( - req, + context, params, apmOptions ); - if (query._debug) { + if (context.params.query._debug) { console.log(`--DEBUG ES QUERY--`); console.log( - `${req.method.toUpperCase()} ${req.url.pathname} ${JSON.stringify( - query - )}` + `${request.url.pathname} ${JSON.stringify(context.params.query)}` ); console.log(`GET ${nextParams.index}/_search`); console.log(JSON.stringify(nextParams.body, null, 4)); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts index 9eada84955d26..ced6f77944b6c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; +import { APMLegacyServer } from '../../routes/typings'; -export function getInternalSavedObjectsClient(server: Server) { +export function getInternalSavedObjectsClient(server: APMLegacyServer) { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; const { callWithInternalUser } = server.plugins.elasticsearch.getCluster( 'admin' diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index 0745c323c7fd2..f320712d6151f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.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 { Legacy } from 'kibana'; import { setupRequest } from './setup_request'; -import { uiSettingsServiceMock } from 'src/core/server/mocks'; +import { APMConfig } from '../../../../../../plugins/apm/server'; +import { APMRequestHandlerContext } from '../../routes/typings'; +import { KibanaRequest } from 'src/core/server'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -15,7 +16,7 @@ jest.mock('../settings/apm_indices/get_apm_indices', () => ({ 'apm_oss.spanIndices': 'apm-*', 'apm_oss.transactionIndices': 'apm-*', 'apm_oss.metricsIndices': 'apm-*', - 'apm_oss.apmAgentConfigurationIndex': 'apm-*' + apmAgentConfigurationIndex: 'apm-*' }) })); @@ -26,37 +27,62 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({ })); function getMockRequest() { - const callWithRequestSpy = jest.fn(); - const callWithInternalUserSpy = jest.fn(); - const mockRequest = ({ - params: {}, - query: {}, - server: { - config: () => ({ get: () => 'apm-*' }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithRequest: callWithRequestSpy, - callWithInternalUser: callWithInternalUserSpy - }) + const mockContext = ({ + config: new Proxy( + {}, + { + get: () => 'apm-*' + } + ) as APMConfig, + params: { + query: { + _debug: false + } + }, + core: { + elasticsearch: { + dataClient: { + callAsCurrentUser: jest.fn(), + callAsInternalUser: jest.fn() } }, - savedObjects: { - getScopedSavedObjectsClient: () => ({ get: async () => false }) + uiSettings: { + client: { + get: jest.fn().mockResolvedValue(false) + } } - }, - getUiSettingsService: () => ({ get: async () => false }) - } as any) as Legacy.Request; + } + } as unknown) as APMRequestHandlerContext & { + core: { + elasticsearch: { + dataClient: { + callAsCurrentUser: jest.Mock; + callAsInternalUser: jest.Mock; + }; + }; + uiSettings: { + client: { + get: jest.Mock; + }; + }; + }; + }; + + const mockRequest = ({ + url: '' + } as unknown) as KibanaRequest; - return { callWithRequestSpy, callWithInternalUserSpy, mockRequest }; + return { mockContext, mockRequest }; } describe('setupRequest', () => { it('should call callWithRequest with default args', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { client } = await setupRequest(mockContext, mockRequest); await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect(callWithRequestSpy).toHaveBeenCalledWith(mockRequest, 'search', { + expect( + mockContext.core.elasticsearch.dataClient.callAsCurrentUser + ).toHaveBeenCalledWith('search', { index: 'apm-*', body: { foo: 'bar', @@ -71,13 +97,15 @@ describe('setupRequest', () => { }); it('should call callWithInternalUser with default args', async () => { - const { mockRequest, callWithInternalUserSpy } = getMockRequest(); - const { internalClient } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); await internalClient.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect(callWithInternalUserSpy).toHaveBeenCalledWith('search', { + expect( + mockContext.core.elasticsearch.dataClient.callAsInternalUser + ).toHaveBeenCalledWith('search', { index: 'apm-*', body: { foo: 'bar', @@ -94,13 +122,15 @@ describe('setupRequest', () => { describe('observer.version_major filter', () => { describe('if index is apm-*', () => { it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { client } = await setupRequest(mockContext, mockRequest); await client.search({ index: 'apm-*', body: { query: { bool: { filter: [{ term: 'someTerm' }] } } } }); - const params = callWithRequestSpy.mock.calls[0][2]; + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.body).toEqual({ query: { bool: { @@ -114,10 +144,12 @@ describe('setupRequest', () => { }); it('should add `observer.version_major` filter if none exists', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { client } = await setupRequest(mockContext, mockRequest); await client.search({ index: 'apm-*' }); - const params = callWithRequestSpy.mock.calls[0][2]; + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.body).toEqual({ query: { bool: { @@ -128,8 +160,8 @@ describe('setupRequest', () => { }); it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { client } = await setupRequest(mockContext, mockRequest); await client.search( { index: 'apm-*', @@ -139,7 +171,9 @@ describe('setupRequest', () => { includeLegacyData: true } ); - const params = callWithRequestSpy.mock.calls[0][2]; + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.body).toEqual({ query: { bool: { filter: [{ term: 'someTerm' }] } } }); @@ -147,15 +181,17 @@ describe('setupRequest', () => { }); it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = await setupRequest(mockRequest); + const { mockContext, mockRequest } = getMockRequest(); + const { client } = await setupRequest(mockContext, mockRequest); await client.search({ index: '.ml-*', body: { query: { bool: { filter: [{ term: 'someTerm' }] } } } }); - const params = callWithRequestSpy.mock.calls[0][2]; + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.body).toEqual({ query: { bool: { @@ -168,28 +204,34 @@ describe('setupRequest', () => { describe('ignore_throttled', () => { it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); + const { mockContext, mockRequest } = getMockRequest(); - const uiSettingsService = uiSettingsServiceMock.createClient(); // mock includeFrozen to return false - uiSettingsService.get.mockResolvedValue(false); - mockRequest.getUiSettingsService = () => uiSettingsService; - const { client } = await setupRequest(mockRequest); + mockContext.core.uiSettings.client.get.mockResolvedValue(false); + + const { client } = await setupRequest(mockContext, mockRequest); + await client.search({}); - const params = callWithRequestSpy.mock.calls[0][2]; + + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.ignore_throttled).toBe(true); }); it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockRequest, callWithRequestSpy } = getMockRequest(); + const { mockContext, mockRequest } = getMockRequest(); - const uiSettingsService = uiSettingsServiceMock.createClient(); // mock includeFrozen to return true - uiSettingsService.get.mockResolvedValue(true); - mockRequest.getUiSettingsService = () => uiSettingsService; - const { client } = await setupRequest(mockRequest); + mockContext.core.uiSettings.client.get.mockResolvedValue(true); + + const { client } = await setupRequest(mockContext, mockRequest); + await client.search({}); - const params = callWithRequestSpy.mock.calls[0][2]; + + const params = + mockContext.core.elasticsearch.dataClient.callAsCurrentUser.mock + .calls[0][1]; expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index c5f76ec51b279..a09cdbf91ec6e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import moment from 'moment'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { getESClient } from './es_client'; -import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; +import { KibanaRequest } from 'src/core/server'; +import { IIndexPattern } from 'src/plugins/data/common'; +import { APMConfig } from '../../../../../../plugins/apm/server'; import { getApmIndices, ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; import { ESClient } from './es_client'; -import { StaticIndexPattern } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; +import { APMRequestHandlerContext } from '../../routes/typings'; +import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; function decodeUiFilters( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFiltersEncoded?: string ) { if (!uiFiltersEncoded || !indexPattern) { @@ -30,52 +30,73 @@ function decodeUiFilters( const uiFilters = JSON.parse(uiFiltersEncoded); return getUiFiltersES(indexPattern, uiFilters); } - -export interface APMRequestQuery { - _debug?: string; - start?: string; - end?: string; - uiFilters?: string; - processorEvent?: ProcessorEvent; -} // Explicitly type Setup to prevent TS initialization errors // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - start: number; - end: number; - uiFiltersES: ESFilter[]; client: ESClient; internalClient: ESClient; - config: KibanaConfig; + config: APMConfig; indices: ApmIndicesConfig; dynamicIndexPattern?: IIndexPattern; } -export async function setupRequest(req: Legacy.Request): Promise { - const query = (req.query as unknown) as APMRequestQuery; - const { server } = req; - const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient( - req - ); - const config = server.config(); - const indices = await getApmIndices({ config, savedObjectsClient }); +export interface SetupTimeRange { + start: number; + end: number; +} +export interface SetupUIFilters { + uiFiltersES: ESFilter[]; +} + +interface SetupRequestParams { + query?: { + _debug?: boolean; + start?: string; + end?: string; + uiFilters?: string; + processorEvent?: ProcessorEvent; + }; +} + +type InferSetup = Setup & + (TParams extends { query: { start: string } } ? { start: number } : {}) & + (TParams extends { query: { end: string } } ? { end: number } : {}) & + (TParams extends { query: { uiFilters: string } } + ? { uiFiltersES: ESFilter[] } + : {}); + +export async function setupRequest( + context: APMRequestHandlerContext, + request: KibanaRequest +): Promise> { + const { config } = context; + const { query } = context.params; + + const indices = await getApmIndices(context); const dynamicIndexPattern = await getDynamicIndexPattern({ - request: req, + context, indices, processorEvent: query.processorEvent }); + const uiFiltersES = decodeUiFilters(dynamicIndexPattern, query.uiFilters); - return { - start: moment.utc(query.start).valueOf(), - end: moment.utc(query.end).valueOf(), - uiFiltersES, - client: getESClient(req, { clientAsInternalUser: false }), - internalClient: getESClient(req, { clientAsInternalUser: true }), - config, + const coreSetupRequest = { indices, + client: getESClient(context, request, { clientAsInternalUser: false }), + internalClient: getESClient(context, request, { + clientAsInternalUser: true + }), + config, dynamicIndexPattern }; + + return { + ...('start' in query ? { start: moment.utc(query.start).valueOf() } : {}), + ...('end' in query ? { end: moment.utc(query.end).valueOf() } : {}), + ...('uiFilters' in query ? { uiFiltersES } : {}), + ...coreSetupRequest + } as InferSetup; } diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index 83294ede080c7..2a31563b53c2c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { createStaticIndexPattern } from './create_static_index_pattern'; import { Setup } from '../helpers/setup_request'; import * as savedObjectsClient from '../helpers/saved_objects_client'; import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data'; +import { APMRequestHandlerContext } from '../../routes/typings'; -function getMockConfig(config: Record) { - return () => ({ get: (key: string) => config[key] }); +function getMockContext(config: Record) { + return ({ + config, + __LEGACY: { + server: { + savedObjects: { + getSavedObjectsRepository: jest.fn() + } + } + } + } as unknown) as APMRequestHandlerContext; } describe('createStaticIndexPattern', () => { @@ -27,46 +36,40 @@ describe('createStaticIndexPattern', () => { it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => { const setup = {} as Setup; - const server = { - config: getMockConfig({ - 'xpack.apm.autocreateApmIndexPattern': false - }) - } as Server; - await createStaticIndexPattern(setup, server); + const context = getMockContext({ + 'xpack.apm.autocreateApmIndexPattern': false + }); + await createStaticIndexPattern(setup, context); expect(createSavedObject).not.toHaveBeenCalled(); }); it(`should not create index pattern if no APM data is found`, async () => { const setup = {} as Setup; - const server = { - config: getMockConfig({ - 'xpack.apm.autocreateApmIndexPattern': true - }) - } as Server; + const context = getMockContext({ + 'xpack.apm.autocreateApmIndexPattern': true + }); // does not have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(false); - await createStaticIndexPattern(setup, server); + await createStaticIndexPattern(setup, context); expect(createSavedObject).not.toHaveBeenCalled(); }); it(`should create index pattern`, async () => { const setup = {} as Setup; - const server = { - config: getMockConfig({ - 'xpack.apm.autocreateApmIndexPattern': true - }) - } as Server; + const context = getMockContext({ + 'xpack.apm.autocreateApmIndexPattern': true + }); // does have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(true); - await createStaticIndexPattern(setup, server); + await createStaticIndexPattern(setup, context); expect(createSavedObject).toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 709d287283932..562eb8850aa0c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; import apmIndexPattern from '../../../../../../../src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_constants'; @@ -11,15 +10,16 @@ import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_const import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server/saved_objects'; import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data'; import { Setup } from '../helpers/setup_request'; +import { APMRequestHandlerContext } from '../../routes/typings'; export async function createStaticIndexPattern( setup: Setup, - server: Server + context: APMRequestHandlerContext ): Promise { - const config = server.config(); + const { config } = context; // don't autocreate APM index pattern if it's been disabled via the config - if (!config.get('xpack.apm.autocreateApmIndexPattern')) { + if (!config['xpack.apm.autocreateApmIndexPattern']) { return; } @@ -31,8 +31,10 @@ export async function createStaticIndexPattern( } try { - const apmIndexPatternTitle = config.get('apm_oss.indexPattern'); - const internalSavedObjectsClient = getInternalSavedObjectsClient(server); + const apmIndexPatternTitle = config['apm_oss.indexPattern']; + const internalSavedObjectsClient = getInternalSavedObjectsClient( + context.__LEGACY.server + ); await internalSavedObjectsClient.create( 'index-pattern', { diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index b0a6eb7aebe8b..9eb99b7c21e75 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { APICaller } from 'src/core/server'; import LRU from 'lru-cache'; import { @@ -14,6 +12,7 @@ import { } from '../../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; import { ProcessorEvent } from '../../../common/processor_event'; +import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ max: 100, @@ -22,11 +21,11 @@ const cache = new LRU({ // TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = async ({ - request, + context, indices, processorEvent }: { - request: Legacy.Request; + context: APMRequestHandlerContext; indices: ApmIndicesConfig; processorEvent?: ProcessorEvent; }) => { @@ -39,9 +38,7 @@ export const getDynamicIndexPattern = async ({ const indexPatternsFetcher = new IndexPatternsFetcher( (...rest: Parameters) => - request.server.plugins.elasticsearch - .getCluster('data') - .callWithRequest(request, ...rest) + context.core.elasticsearch.adminClient.callAsCurrentUser(...rest) ); // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) @@ -53,7 +50,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices }); - const indexPattern: StaticIndexPattern = { + const indexPattern: IIndexPattern = { fields, title: indexPatternTitle }; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/default.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/default.ts index f55d3320c7f4e..72aa93d7f6ce3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/default.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/default.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { getCPUChartData } from './shared/cpu'; import { getMemoryChartData } from './shared/memory'; export async function getDefaultMetricsCharts( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string ) { const charts = await Promise.all([ diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts index 180537d68a2a2..8ffc115a19348 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts @@ -11,7 +11,11 @@ import { sum, round } from 'lodash'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; import { getMetricsProjection } from '../../../../../../common/projections/metrics'; @@ -23,16 +27,7 @@ import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; import { getBucketSize } from '../../../../helpers/get_bucket_size'; - -const colors = [ - theme.euiColorVis0, - theme.euiColorVis1, - theme.euiColorVis2, - theme.euiColorVis3, - theme.euiColorVis4, - theme.euiColorVis5, - theme.euiColorVis6 -]; +import { getVizColorForIndex } from '../../../../../../common/viz_colors'; export async function fetchAndTransformGcMetrics({ setup, @@ -41,7 +36,7 @@ export async function fetchAndTransformGcMetrics({ chartBase, fieldName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; serviceNodeName?: string; chartBase: ChartBase; @@ -148,7 +143,7 @@ export async function fetchAndTransformGcMetrics({ title: label, key: label, type: chartBase.type, - color: colors[i], + color: getVizColorForIndex(i, theme), overallValue, data }; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts index 642c6a901da9d..21417891fa15f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts @@ -7,7 +7,11 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { fetchAndTransformGcMetrics } from './fetchAndTransformGcMetrics'; import { ChartBase } from '../../../types'; @@ -31,7 +35,7 @@ const chartBase: ChartBase = { }; const getGcRateChart = ( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) => { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts index b6e992acf62a9..ea7557fabacd0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts @@ -7,7 +7,11 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { fetchAndTransformGcMetrics } from './fetchAndTransformGcMetrics'; import { ChartBase } from '../../../types'; @@ -31,7 +35,7 @@ const chartBase: ChartBase = { }; const getGcTimeChart = ( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) => { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index ca98a486b3a58..901812815b3f3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -12,7 +12,11 @@ import { METRIC_JAVA_HEAP_MEMORY_USED, SERVICE_AGENT_NAME } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; import { ChartBase } from '../../../types'; @@ -51,7 +55,7 @@ const chartBase: ChartBase = { }; export async function getHeapMemoryChart( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts index e8f9e4345d06c..191a7a2c14d23 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -5,7 +5,11 @@ */ import { getHeapMemoryChart } from './heap_memory'; -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; import { getNonHeapMemoryChart } from './non_heap_memory'; import { getThreadCountChart } from './thread_count'; import { getCPUChartData } from '../shared/cpu'; @@ -14,7 +18,7 @@ import { getGcRateChart } from './gc/getGcRateChart'; import { getGcTimeChart } from './gc/getGcTimeChart'; export async function getJavaMetricsCharts( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index 899852e1d5659..7ff4e073e919b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -12,7 +12,11 @@ import { METRIC_JAVA_NON_HEAP_MEMORY_USED, SERVICE_AGENT_NAME } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; @@ -48,7 +52,7 @@ const chartBase: ChartBase = { }; export async function getNonHeapMemoryChart( - setup: Setup, + setup: Setup & SetupUIFilters & SetupTimeRange, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index eb0984d7aaf59..cf8e120b00e0d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -10,7 +10,11 @@ import { METRIC_JAVA_THREAD_COUNT, SERVICE_AGENT_NAME } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; @@ -40,7 +44,7 @@ const chartBase: ChartBase = { }; export async function getThreadCountChart( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index 5f4ad0af474c1..179ed77eedbb3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -10,7 +10,11 @@ import { METRIC_SYSTEM_CPU_PERCENT, METRIC_PROCESS_CPU_PERCENT } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; @@ -52,7 +56,7 @@ const chartBase: ChartBase = { }; export async function getCPUChartData( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index bf0f21dcb3d42..8c6ed2ebcec75 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -9,7 +9,11 @@ import { METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY } from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../../helpers/setup_request'; import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; @@ -45,7 +49,7 @@ const percentUsedScript = { }; export async function getMemoryChartData( - setup: Setup, + setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 3d425e50bc60a..76460bb40bedb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,11 @@ */ import { Unionize } from 'utility-types'; -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; @@ -39,7 +43,7 @@ export async function fetchAndTransformMetrics({ aggs, additionalFilters = [] }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; serviceNodeName?: string; chartBase: ChartBase; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts index 34c8b2f867fbb..e0b496754fc9e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts @@ -3,7 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getJavaMetricsCharts } from './by_agent/java'; import { getDefaultMetricsCharts } from './by_agent/default'; import { GenericMetricsChart } from './transform_metrics_chart'; @@ -18,7 +22,7 @@ export async function getMetricsChartDataByAgent({ serviceNodeName, agentName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; serviceNodeName?: string; agentName: string; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 1e7f197435a67..03f21e4f26e7b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -11,16 +11,7 @@ import { ESSearchRequest } from '../../../typings/elasticsearch'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; - -const colors = [ - theme.euiColorVis0, - theme.euiColorVis1, - theme.euiColorVis2, - theme.euiColorVis3, - theme.euiColorVis4, - theme.euiColorVis5, - theme.euiColorVis6 -]; +import { getVizColorForIndex } from '../../../common/viz_colors'; export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart @@ -66,7 +57,8 @@ export function transformDataToMetricsChart( title: chartBase.series[seriesKey].title, key: seriesKey, type: chartBase.type, - color: chartBase.series[seriesKey].color || colors[i], + color: + chartBase.series[seriesKey].color || getVizColorForIndex(i, theme), overallValue, data: timeseriesData?.buckets.map(bucket => { diff --git a/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts index 1e415252200ce..b674afe635bcd 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; @@ -19,7 +23,7 @@ const getServiceNodes = async ({ setup, serviceName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { const { client } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts index 9e070f936a25f..a1a2c1a38b3d4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -9,9 +9,12 @@ import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export async function getServiceAgentName(serviceName: string, setup: Setup) { +export async function getServiceAgentName( + serviceName: string, + setup: Setup & SetupTimeRange +) { const { start, end, client, indices } = setup; const params = { diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts index e93f6b4a1c17c..7120d3bca6c25 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { HOST_NAME, CONTAINER_ID @@ -20,7 +24,7 @@ export async function getServiceNodeMetadata({ }: { serviceName: string; serviceNodeName: string; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { client } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts index 098342bf0221d..60f6e63bb7b25 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -9,11 +9,11 @@ import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceTransactionTypes( serviceName: string, - setup: Setup + setup: Setup & SetupTimeRange ) { const { start, end, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts index 6611ae76bc339..8e578a839ae56 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -12,11 +12,17 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../../typings/common'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { getServicesProjection } from '../../../../common/projections/services'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServicesItems(setup: Setup) { +export async function getServicesItems( + setup: Setup & SetupTimeRange & SetupUIFilters +) { const { start, end, client } = setup; const projection = getServicesProjection({ setup }); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/index.ts index ffa9555c29070..d9fc89062cf88 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/index.ts @@ -6,26 +6,33 @@ import { isEmpty } from 'lodash'; import { PromiseReturnType } from '../../../../typings/common'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { hasHistoricalAgentData } from './has_historical_agent_data'; import { getLegacyDataStatus } from './get_legacy_data_status'; import { getServicesItems } from './get_services_items'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServices(setup: Setup) { - const items = await getServicesItems(setup); - const hasLegacyData = await getLegacyDataStatus(setup); - // conditionally check for historical data if no services were found in the current time range +export async function getServices( + setup: Setup & SetupTimeRange & SetupUIFilters +) { + const [items, hasLegacyData] = await Promise.all([ + getServicesItems(setup), + getLegacyDataStatus(setup) + ]); + const noDataInCurrentTimeRange = isEmpty(items); - let hasHistorialAgentData = true; - if (noDataInCurrentTimeRange) { - hasHistorialAgentData = await hasHistoricalAgentData(setup); - } + const hasHistoricalData = noDataInCurrentTimeRange + ? await hasHistoricalAgentData(setup) + : true; return { items, - hasHistoricalData: hasHistorialAgentData, + hasHistoricalData, hasLegacyData }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 434eda8c0f46e..52ba22cbc0b99 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,31 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/server'; +import { IClusterClient } from 'src/core/server'; +import { APMConfig } from '../../../../../../../plugins/apm/server'; import { CallCluster } from '../../../../../../../../src/legacy/core_plugins/elasticsearch'; -import { getApmIndices } from '../apm_indices/get_apm_indices'; -import { LegacySetup } from '../../../new-platform/plugin'; -import { getInternalSavedObjectsClient } from '../../helpers/saved_objects_client'; +import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; -export async function createApmAgentConfigurationIndex( - core: CoreSetup, - { server }: LegacySetup -) { +export async function createApmAgentConfigurationIndex({ + esClient, + config +}: { + esClient: IClusterClient; + config: APMConfig; +}) { try { - const config = server.config(); - const internalSavedObjectsClient = getInternalSavedObjectsClient(server); - const indices = await getApmIndices({ - savedObjectsClient: internalSavedObjectsClient, - config - }); - const index = indices['apm_oss.apmAgentConfigurationIndex']; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - const indexExists = await callWithInternalUser('indices.exists', { index }); + const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; + const { callAsInternalUser } = esClient; + const indexExists = await callAsInternalUser('indices.exists', { index }); const result = indexExists - ? await updateExistingIndex(index, callWithInternalUser) - : await createNewIndex(index, callWithInternalUser); + ? await updateExistingIndex(index, callAsInternalUser) + : await createNewIndex(index, callAsInternalUser); if (!result.acknowledged) { const resultError = diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 23faa4b74cf8f..5a67f78de6f65 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -25,7 +25,7 @@ export async function createOrUpdateConfiguration({ const params: APMIndexDocumentParams = { refresh: true, - index: indices['apm_oss.apmAgentConfigurationIndex'], + index: indices.apmAgentConfigurationIndex, body: { agent_name: configuration.agent_name, service: { diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts index ed20a58b271e1..293c01d4b61d5 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts @@ -17,7 +17,7 @@ export async function deleteConfiguration({ const params = { refresh: 'wait_for', - index: indices['apm_oss.apmAgentConfigurationIndex'], + index: indices.apmAgentConfigurationIndex, id: configurationId }; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index fc3b62738f8fe..f54217461510f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -25,7 +25,7 @@ export async function getExistingEnvironmentsForService({ : { must_not: [{ exists: { field: SERVICE_NAME } }] }; const params = { - index: indices['apm_oss.apmAgentConfigurationIndex'], + index: indices.apmAgentConfigurationIndex, body: { size: 0, query: { bool }, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index dd4d019ef7263..12faa9fba1074 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -15,7 +15,7 @@ export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; const params = { - index: indices['apm_oss.apmAgentConfigurationIndex'] + index: indices.apmAgentConfigurationIndex }; const resp = await internalClient.search(params); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index b7b9c21172140..b6aecd1d7f0ca 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -19,7 +19,7 @@ export async function markAppliedByAgent({ const { internalClient, indices } = setup; const params = { - index: indices['apm_oss.apmAgentConfigurationIndex'], + index: indices.apmAgentConfigurationIndex, id, // by specifying the `id` elasticsearch will do an "upsert" body: { ...body, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts index 969bbc542f8a6..a02dd7af755e0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts @@ -33,7 +33,7 @@ export async function searchConfigurations({ : []; const params = { - index: indices['apm_oss.apmAgentConfigurationIndex'], + index: indices.apmAgentConfigurationIndex, body: { query: { bool: { diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index e942a26da373e..0ed30ec4cdd27 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -5,13 +5,15 @@ */ import { merge } from 'lodash'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { Server } from 'hapi'; +import { SavedObjectsClientContract } from 'kibana/server'; import { PromiseReturnType } from '../../../../typings/common'; import { APM_INDICES_SAVED_OBJECT_TYPE, APM_INDICES_SAVED_OBJECT_ID } from '../../../../common/apm_saved_object_constants'; +import { APMConfig } from '../../../../../../../plugins/apm/server'; +import { APMRequestHandlerContext } from '../../../routes/typings'; export interface ApmIndicesConfig { 'apm_oss.sourcemapIndices': string; @@ -20,7 +22,7 @@ export interface ApmIndicesConfig { 'apm_oss.spanIndices': string; 'apm_oss.transactionIndices': string; 'apm_oss.metricsIndices': string; - 'apm_oss.apmAgentConfigurationIndex': string; + apmAgentConfigurationIndex: string; } export type ApmIndicesName = keyof ApmIndicesConfig; @@ -30,7 +32,7 @@ export type ScopedSavedObjectsClient = ReturnType< >; async function getApmIndicesSavedObject( - savedObjectsClient: ScopedSavedObjectsClient + savedObjectsClient: SavedObjectsClientContract ) { const apmIndices = await savedObjectsClient.get>( APM_INDICES_SAVED_OBJECT_TYPE, @@ -39,39 +41,28 @@ async function getApmIndicesSavedObject( return apmIndices.attributes; } -function getApmIndicesConfig(config: KibanaConfig): ApmIndicesConfig { +export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { return { - 'apm_oss.sourcemapIndices': config.get('apm_oss.sourcemapIndices'), - 'apm_oss.errorIndices': config.get('apm_oss.errorIndices'), - 'apm_oss.onboardingIndices': config.get( - 'apm_oss.onboardingIndices' - ), - 'apm_oss.spanIndices': config.get('apm_oss.spanIndices'), - 'apm_oss.transactionIndices': config.get( - 'apm_oss.transactionIndices' - ), - 'apm_oss.metricsIndices': config.get('apm_oss.metricsIndices'), - 'apm_oss.apmAgentConfigurationIndex': config.get( - 'apm_oss.apmAgentConfigurationIndex' - ) + 'apm_oss.sourcemapIndices': config['apm_oss.sourcemapIndices'], + 'apm_oss.errorIndices': config['apm_oss.errorIndices'], + 'apm_oss.onboardingIndices': config['apm_oss.onboardingIndices'], + 'apm_oss.spanIndices': config['apm_oss.spanIndices'], + 'apm_oss.transactionIndices': config['apm_oss.transactionIndices'], + 'apm_oss.metricsIndices': config['apm_oss.metricsIndices'], + // system indices, not configurable + apmAgentConfigurationIndex: '.apm-agent-configuration' }; } -export async function getApmIndices({ - savedObjectsClient, - config -}: { - savedObjectsClient: ScopedSavedObjectsClient; - config: KibanaConfig; -}) { +export async function getApmIndices(context: APMRequestHandlerContext) { try { const apmIndicesSavedObject = await getApmIndicesSavedObject( - savedObjectsClient + context.core.savedObjects.client ); - const apmIndicesConfig = getApmIndicesConfig(config); + const apmIndicesConfig = getApmIndicesConfig(context.config); return merge({}, apmIndicesConfig, apmIndicesSavedObject); } catch (error) { - return getApmIndicesConfig(config); + return getApmIndicesConfig(context.config); } } @@ -81,20 +72,19 @@ const APM_UI_INDICES: ApmIndicesName[] = [ 'apm_oss.onboardingIndices', 'apm_oss.spanIndices', 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices', - 'apm_oss.apmAgentConfigurationIndex' + 'apm_oss.metricsIndices' ]; export async function getApmIndexSettings({ - config, - savedObjectsClient + context }: { - config: KibanaConfig; - savedObjectsClient: ScopedSavedObjectsClient; + context: APMRequestHandlerContext; }) { let apmIndicesSavedObject: PromiseReturnType; try { - apmIndicesSavedObject = await getApmIndicesSavedObject(savedObjectsClient); + apmIndicesSavedObject = await getApmIndicesSavedObject( + context.core.savedObjects.client + ); } catch (error) { if (error.output && error.output.statusCode === 404) { apmIndicesSavedObject = {}; @@ -102,7 +92,7 @@ export async function getApmIndexSettings({ throw error; } } - const apmIndicesConfig = getApmIndicesConfig(config); + const apmIndicesConfig = getApmIndicesConfig(context.config); return APM_UI_INDICES.map(configurationName => ({ configurationName, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts index e57e64942ab89..2fdfd79ce933b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ApmIndicesConfig, ScopedSavedObjectsClient } from './get_apm_indices'; import { APM_INDICES_SAVED_OBJECT_TYPE, APM_INDICES_SAVED_OBJECT_ID } from '../../../../common/apm_saved_object_constants'; +import { ApmIndicesConfig } from './get_apm_indices'; +import { APMRequestHandlerContext } from '../../../routes/typings'; export async function saveApmIndices( - savedObjectsClient: ScopedSavedObjectsClient, + context: APMRequestHandlerContext, apmIndicesSavedObject: Partial ) { - return await savedObjectsClient.create( + return await context.core.savedObjects.client.create( APM_INDICES_SAVED_OBJECT_TYPE, apmIndicesSavedObject, { diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts index a296bcbdecccf..e38ce56edde80 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts @@ -6,11 +6,11 @@ import { PromiseReturnType } from '../../../typings/common'; import { getTraceErrorsPerTransaction } from '../errors/get_trace_errors_per_transaction'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTraceItems } from './get_trace_items'; export type TraceAPIResponse = PromiseReturnType; -export async function getTrace(traceId: string, setup: Setup) { +export async function getTrace(traceId: string, setup: Setup & SetupTimeRange) { const [trace, errorsPerTransaction] = await Promise.all([ getTraceItems(traceId, setup), getTraceErrorsPerTransaction(traceId, setup) diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts index 0df5cc016431d..8ea548ab3724b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts @@ -14,11 +14,14 @@ import { import { Span } from '../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export async function getTraceItems(traceId: string, setup: Setup) { +export async function getTraceItems( + traceId: string, + setup: Setup & SetupTimeRange +) { const { start, end, client, config, indices } = setup; - const maxTraceItems = config.get('xpack.apm.ui.maxTraceItems'); + const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const params = { index: [ diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index 1d849fbbaaaf5..4121ff74bfacc 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -5,6 +5,7 @@ */ import { transactionGroupsFetcher } from './fetcher'; +import { APMConfig } from '../../../../../../plugins/apm/server'; function getSetup() { return { @@ -17,14 +18,8 @@ function getSetup() { search: jest.fn() } as any, config: { - get: jest.fn((key: string) => { - switch (key) { - case 'xpack.apm.ui.transactionGroupBucketSize': - return 100; - } - }), - has: () => true - }, + 'xpack.apm.ui.transactionGroupBucketSize': 100 + } as APMConfig, uiFiltersES: [{ term: { 'service.environment': 'test' } }], indices: { 'apm_oss.sourcemapIndices': 'myIndex', @@ -33,7 +28,7 @@ function getSetup() { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - 'apm_oss.apmAgentConfigurationIndex': 'myIndex' + apmAgentConfigurationIndex: 'myIndex' }, dynamicIndexPattern: null as any }; diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts index bfa46abcad36f..b08bdc334fc87 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -9,7 +9,11 @@ import { TRANSACTION_SAMPLED } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; @@ -30,7 +34,10 @@ interface TopTraceOptions { export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export function transactionGroupsFetcher(options: Options, setup: Setup) { +export function transactionGroupsFetcher( + options: Options, + setup: Setup & SetupTimeRange & SetupUIFilters +) { const { client, config } = setup; const projection = getTransactionGroupsProjection({ @@ -57,7 +64,7 @@ export function transactionGroupsFetcher(options: Options, setup: Setup) { terms: { ...projection.body.aggs.transactions.terms, order: { sum: 'desc' as const }, - size: config.get('xpack.apm.ui.transactionGroupBucketSize') + size: config['xpack.apm.ui.transactionGroupBucketSize'] }, aggs: { sample: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts index 73e30f28c4206..3656b32c17092 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; import { transactionGroupsFetcher, Options } from './fetcher'; import { transactionGroupsTransformer } from './transform'; import { PromiseReturnType } from '../../../typings/common'; @@ -12,7 +16,10 @@ import { PromiseReturnType } from '../../../typings/common'; export type TransactionGroupListAPIResponse = PromiseReturnType< typeof getTransactionGroupList >; -export async function getTransactionGroupList(options: Options, setup: Setup) { +export async function getTransactionGroupList( + options: Options, + setup: Setup & SetupTimeRange & SetupUIFilters +) { const { start, end } = setup; const response = await transactionGroupsFetcher(options, setup); return transactionGroupsTransformer({ diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts new file mode 100644 index 0000000000000..3f0f8a84dc62f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ESSearchResponse, + ESSearchRequest +} from '../../../../../typings/elasticsearch'; + +export const response = ({ + hits: { + total: 599, + max_score: 0, + hits: [] + }, + took: 4, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + aggregations: { + user_agent_keys: { + buckets: [{ key: 'Firefox' }, { key: 'Other' }] + }, + browsers: { + buckets: [ + { + key_as_string: '2019-10-21T04:38:20.000-05:00', + key: 1571650700000, + doc_count: 0, + user_agent: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [] + } + }, + { + key_as_string: '2019-10-21T04:40:00.000-05:00', + key: 1571650800000, + doc_count: 1, + user_agent: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Other', + doc_count: 1, + avg_duration: { + value: 860425.0 + } + }, + { + key: 'Firefox', + doc_count: 10, + avg_duration: { + value: 86425.1 + } + } + ] + } + } + ] + } + } +} as unknown) as ESSearchResponse< + unknown, + ESSearchRequest, + { restTotalHitsAsInt: false } +>; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts new file mode 100644 index 0000000000000..1a5921e06d0d1 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; +import { fetcher } from './fetcher'; + +describe('fetcher', () => { + it('performs a search', async () => { + const search = jest.fn(); + const setup = ({ + client: { search }, + indices: {}, + uiFiltersES: [] + } as unknown) as Setup & SetupTimeRange & SetupUIFilters; + + await fetcher({ serviceName: 'testServiceName', setup }); + + expect(search).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts new file mode 100644 index 0000000000000..8a96a25aef50e --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESFilter } from '../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../typings/common'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, + USER_AGENT_NAME, + TRANSACTION_DURATION +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../helpers/range_filter'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Options } from '.'; +import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; +import { ProcessorEvent } from '../../../../common/processor_event'; + +export type ESResponse = PromiseReturnType; + +export function fetcher(options: Options) { + const { end, client, indices, start, uiFiltersES } = options.setup; + const { serviceName } = options; + const { intervalString } = getBucketSize(start, end, 'auto'); + + const filter: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES + ]; + + const params = { + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { bool: { filter } }, + aggs: { + user_agent_keys: { + terms: { + field: USER_AGENT_NAME + } + }, + browsers: { + date_histogram: { + extended_bounds: { + max: end, + min: start + }, + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0 + }, + aggs: { + user_agent: { + terms: { + field: USER_AGENT_NAME + }, + aggs: { + avg_duration: { + avg: { + field: TRANSACTION_DURATION + } + } + } + } + } + } + } + } + }; + + return client.search(params); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts new file mode 100644 index 0000000000000..fe103ade24161 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getTransactionAvgDurationByBrowser, + Options, + AvgDurationByBrowserAPIResponse +} from '.'; +import * as transformerModule from './transformer'; +import * as fetcherModule from './fetcher'; +import { response } from './__fixtures__/responses'; + +describe('getAvgDurationByBrowser', () => { + it('returns a transformed response', async () => { + const transformer = jest + .spyOn(transformerModule, 'transformer') + .mockReturnValueOnce(({} as unknown) as AvgDurationByBrowserAPIResponse); + const search = () => {}; + const options = ({ + setup: { client: { search }, indices: {}, uiFiltersES: [] } + } as unknown) as Options; + jest + .spyOn<{ fetcher: any }, 'fetcher'>(fetcherModule, 'fetcher') + .mockResolvedValueOnce(response); + + await getTransactionAvgDurationByBrowser(options); + + expect(transformer).toHaveBeenCalledWith({ response }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts new file mode 100644 index 0000000000000..07b598a86caa4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Coordinate } from '../../../../typings/timeseries'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; +import { fetcher } from './fetcher'; +import { transformer } from './transformer'; + +export interface Options { + serviceName: string; + setup: Setup & SetupTimeRange & SetupUIFilters; +} + +export type AvgDurationByBrowserAPIResponse = Array<{ + data: Coordinate[]; + title: string; +}>; + +export async function getTransactionAvgDurationByBrowser(options: Options) { + return transformer({ response: await fetcher(options) }); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts new file mode 100644 index 0000000000000..5caec12c81d5d --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { transformer } from './transformer'; +import { response } from './__fixtures__/responses'; + +describe('transformer', () => { + it('transforms', () => { + expect(transformer({ response })).toEqual([ + { + data: [ + { x: 1571650700000, y: undefined }, + { x: 1571650800000, y: 86425.1 } + ], + title: 'Firefox' + }, + { + data: [ + { x: 1571650700000, y: undefined }, + { x: 1571650800000, y: 860425.0 } + ], + title: 'Other' + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts new file mode 100644 index 0000000000000..5d140155f75e4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESResponse } from './fetcher'; +import { AvgDurationByBrowserAPIResponse } from '.'; +import { Coordinate } from '../../../../typings/timeseries'; + +export function transformer({ + response +}: { + response: ESResponse; +}): AvgDurationByBrowserAPIResponse { + const allUserAgentKeys = new Set( + (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => + key.toString() + ) + ); + const buckets = response.aggregations?.browsers?.buckets ?? []; + + const series = buckets.reduce<{ [key: string]: Coordinate[] }>( + (acc, next) => { + const userAgentBuckets = next.user_agent?.buckets ?? []; + const x = next.key; + const seenUserAgentKeys = new Set(); + + userAgentBuckets.map(userAgentBucket => { + const key = userAgentBucket.key; + const y = userAgentBucket.avg_duration?.value; + + seenUserAgentKeys.add(key.toString()); + acc[key] = (acc[key] || []).concat({ x, y }); + }); + + const emptyUserAgents = new Set( + [...allUserAgentKeys].filter(key => !seenUserAgentKeys.has(key)) + ); + + // If no user agent requests exist for this bucked, fill in the data with + // undefined + [...emptyUserAgents].map(key => { + acc[key] = (acc[key] || []).concat({ x, y: undefined }); + }); + + return acc; + }, + {} + ); + + return Object.entries(series).map(([title, data]) => ({ title, data })); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ed6bdf203f2d4..e2dfb5d0f7a58 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -12,7 +12,11 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; @@ -21,7 +25,7 @@ export async function getTransactionAvgDurationByCountry({ serviceName, transactionName }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; transactionName?: string; }) { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts index 0e288de1e4600..dcf6e8e07c45b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts @@ -3,19 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; export const MAX_KPIS = 20; - -export const COLORS = [ - theme.euiColorVis0, - theme.euiColorVis1, - theme.euiColorVis2, - theme.euiColorVis3, - theme.euiColorVis4, - theme.euiColorVis5, - theme.euiColorVis6, - theme.euiColorVis7, - theme.euiColorVis8, - theme.euiColorVis9 -]; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 8254f028923d3..f49c1e022a070 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -8,6 +8,7 @@ import { getTransactionBreakdown } from '.'; import * as constants from './constants'; import noDataResponse from './mock-responses/noData.json'; import dataResponse from './mock-responses/data.json'; +import { APMConfig } from '../../../../../../../plugins/apm/server'; const mockIndices = { 'apm_oss.sourcemapIndices': 'myIndex', @@ -16,7 +17,7 @@ const mockIndices = { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - 'apm_oss.apmAgentConfigurationIndex': 'myIndex' + apmAgentConfigurationIndex: 'myIndex' }; function getMockSetup(esResponse: any) { @@ -26,10 +27,12 @@ function getMockSetup(esResponse: any) { end: 500000, client: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, - config: { - get: () => 'myIndex' as any, - has: () => true - }, + config: new Proxy( + {}, + { + get: () => 'myIndex' + } + ) as APMConfig, uiFiltersES: [], indices: mockIndices, dynamicIndexPattern: null as any diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index 3166938090d8f..26e62d8902eeb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -15,10 +15,15 @@ import { TRANSACTION_BREAKDOWN_COUNT, PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; import { getMetricsDateHistogramParams } from '../../helpers/metrics'; -import { MAX_KPIS, COLORS } from './constants'; +import { MAX_KPIS } from './constants'; +import { getVizColorForIndex } from '../../../../common/viz_colors'; export async function getTransactionBreakdown({ setup, @@ -26,7 +31,7 @@ export async function getTransactionBreakdown({ transactionName, transactionType }: { - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; transactionName?: string; transactionType: string; @@ -142,7 +147,7 @@ export async function getTransactionBreakdown({ const kpis = sortByOrder(visibleKpis, 'name').map((kpi, index) => { return { ...kpi, - color: COLORS[index % COLORS.length] + color: getVizColorForIndex(index) }; }); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts index 9b51d9336d2e6..5f211b1427259 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts @@ -6,7 +6,7 @@ import { getMlIndex } from '../../../../../common/ml_job_constants'; import { PromiseReturnType } from '../../../../../typings/common'; -import { Setup } from '../../../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; export type ESResponse = Exclude< PromiseReturnType, @@ -24,7 +24,7 @@ export async function anomalySeriesFetcher({ transactionType: string; intervalString: string; mlBucketSize: number; - setup: Setup; + setup: Setup & SetupTimeRange; }) { const { client, start, end } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts index 4933f1b1ed431..9419fa7a77fb9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts @@ -5,12 +5,12 @@ */ import { getMlIndex } from '../../../../../common/ml_job_constants'; -import { Setup } from '../../../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; interface IOptions { serviceName: string; transactionType: string; - setup: Setup; + setup: Setup & SetupTimeRange; } interface ESResponse { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts index a5008ce10f9f9..2a56f744f2f45 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts @@ -8,6 +8,7 @@ import { getAnomalySeries } from '.'; import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse'; import { mlBucketSpanResponse } from './mock-responses/mlBucketSpanResponse'; import { PromiseReturnType } from '../../../../../typings/common'; +import { APMConfig } from '../../../../../../../../plugins/apm/server'; describe('getAnomalySeries', () => { let avgAnomalies: PromiseReturnType; @@ -27,10 +28,12 @@ describe('getAnomalySeries', () => { end: 500000, client: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, - config: { - get: () => 'myIndex' as any, - has: () => true - }, + config: new Proxy( + {}, + { + get: () => 'myIndex' + } + ) as APMConfig, uiFiltersES: [], indices: { 'apm_oss.sourcemapIndices': 'myIndex', @@ -39,7 +42,7 @@ describe('getAnomalySeries', () => { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - 'apm_oss.apmAgentConfigurationIndex': 'myIndex' + apmAgentConfigurationIndex: 'myIndex' }, dynamicIndexPattern: null as any } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index b55d264cfbbfe..c631772b0e18c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -5,7 +5,11 @@ */ import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; import { anomalySeriesFetcher } from './fetcher'; import { getMlBucketSize } from './get_ml_bucket_size'; import { anomalySeriesTransform } from './transform'; @@ -21,7 +25,7 @@ export async function getAnomalySeries({ transactionType: string | undefined; transactionName: string | undefined; timeSeriesDates: number[]; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { // don't fetch anomalies for transaction details page if (transactionName) { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index 7bdfb17ba17b3..676ad4ded6b69 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -6,6 +6,7 @@ import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; +import { APMConfig } from '../../../../../../../../plugins/apm/server'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -22,10 +23,12 @@ describe('timeseriesFetcher', () => { end: 1528977600000, client: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, - config: { - get: () => 'myIndex' as any, - has: () => true - }, + config: new Proxy( + {}, + { + get: () => 'myIndex' + } + ) as APMConfig, uiFiltersES: [ { term: { 'service.environment': 'test' } @@ -38,7 +41,7 @@ describe('timeseriesFetcher', () => { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - 'apm_oss.apmAgentConfigurationIndex': 'myIndex' + apmAgentConfigurationIndex: 'myIndex' }, dynamicIndexPattern: null as any } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 0d9cccb3b56d3..8a2e01c9a7891 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -16,7 +16,11 @@ import { import { PromiseReturnType } from '../../../../../typings/common'; import { getBucketSize } from '../../../helpers/get_bucket_size'; import { rangeFilter } from '../../../helpers/range_filter'; -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; export type ESResponse = PromiseReturnType; export function timeseriesFetcher({ @@ -28,7 +32,7 @@ export function timeseriesFetcher({ serviceName: string; transactionType: string | undefined; transactionName: string | undefined; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, client, indices } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 6c18ab84cdfab..96d06bdd3b0e1 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -5,7 +5,11 @@ */ import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; import { timeseriesFetcher } from './fetcher'; import { timeseriesTransformer } from './transform'; @@ -13,7 +17,7 @@ export async function getApmTimeseriesData(options: { serviceName: string; transactionType: string | undefined; transactionName: string | undefined; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end } = options.setup; const { bucketSize } = getBucketSize(start, end, 'auto'); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts index c297f3a050f0c..a6a1a76e19664 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts @@ -5,7 +5,11 @@ */ import { PromiseReturnType } from '../../../../typings/common'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { getAnomalySeries } from './get_anomaly_data'; import { getApmTimeseriesData } from './get_timeseries_data'; import { ApmTimeSeriesResponse } from './get_timeseries_data/transform'; @@ -21,7 +25,7 @@ export async function getTransactionCharts(options: { serviceName: string; transactionType: string | undefined; transactionName: string | undefined; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const apmTimeseries = await getApmTimeseriesData(options); const anomalyTimeseries = await getAnomalySeries({ diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/constants.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/constants.ts new file mode 100644 index 0000000000000..7fae088048903 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const MINIMUM_BUCKET_SIZE = 15; +export const BUCKET_TARGET_COUNT = 15; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 90d9a925a1f36..32fa65722869d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -16,7 +16,11 @@ import { TRANSACTION_TYPE } from '../../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../helpers/range_filter'; -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; export async function bucketFetcher( serviceName: string, @@ -26,7 +30,7 @@ export async function bucketFetcher( traceId: string, distributionMax: number, bucketSize: number, - setup: Setup + setup: Setup & SetupTimeRange & SetupUIFilters ) { const { start, end, uiFiltersES, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 86429986063ed..90b2fa5894665 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../../helpers/setup_request'; import { bucketFetcher } from './fetcher'; import { bucketTransformer } from './transform'; @@ -16,7 +20,7 @@ export async function getBuckets( traceId: string, distributionMax: number, bucketSize: number, - setup: Setup + setup: Setup & SetupTimeRange & SetupUIFilters ) { const response = await bucketFetcher( serviceName, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index a54fa9c10de13..0dfe769c0bbac 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -11,13 +11,17 @@ import { TRANSACTION_NAME, TRANSACTION_TYPE } from '../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; export async function getDistributionMax( serviceName: string, transactionName: string, transactionType: string, - setup: Setup + setup: Setup & SetupTimeRange & SetupUIFilters ) { const { start, end, uiFiltersES, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts index 3efa996d609d8..9dd29a0664329 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts @@ -5,19 +5,20 @@ */ import { PromiseReturnType } from '../../../../typings/common'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; import { getBuckets } from './get_buckets'; import { getDistributionMax } from './get_distribution_max'; import { roundToNearestFiveOrTen } from '../../helpers/round_to_nearest_five_or_ten'; +import { MINIMUM_BUCKET_SIZE, BUCKET_TARGET_COUNT } from '../constants'; -function getBucketSize(max: number, { config }: Setup) { - const minBucketSize: number = config.get( - 'xpack.apm.minimumBucketSize' - ); - const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); - const bucketSize = max / bucketTargetCount; +function getBucketSize(max: number) { + const bucketSize = max / BUCKET_TARGET_COUNT; return roundToNearestFiveOrTen( - bucketSize > minBucketSize ? bucketSize : minBucketSize + bucketSize > MINIMUM_BUCKET_SIZE ? bucketSize : MINIMUM_BUCKET_SIZE ); } @@ -37,7 +38,7 @@ export async function getTransactionDistribution({ transactionType: string; transactionId: string; traceId: string; - setup: Setup; + setup: Setup & SetupTimeRange & SetupUIFilters; }) { const distributionMax = await getDistributionMax( serviceName, @@ -50,7 +51,7 @@ export async function getTransactionDistribution({ return { noHits: true, buckets: [], bucketSize: 0 }; } - const bucketSize = getBucketSize(distributionMax, setup); + const bucketSize = getBucketSize(distributionMax); const { buckets, noHits } = await getBuckets( serviceName, transactionName, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts index 652acf773e2e5..56cee04049bd9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -11,12 +11,16 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { rangeFilter } from '../../helpers/range_filter'; -import { Setup } from '../../helpers/setup_request'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../../helpers/setup_request'; export async function getTransaction( transactionId: string, traceId: string, - setup: Setup + setup: Setup & SetupTimeRange & SetupUIFilters ) { const { start, end, uiFiltersES, client, indices } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts index 1b9e2ebd2e757..50c1926d1e4a0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -10,11 +10,14 @@ import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ESFilter } from '../../../typings/elasticsearch'; -export async function getEnvironments(setup: Setup, serviceName?: string) { +export async function getEnvironments( + setup: Setup & SetupTimeRange, + serviceName?: string +) { const { start, end, client, indices } = setup; const filter: ESFilter[] = [ diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts index cc1ebf2d51952..0bf89414e2894 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts @@ -11,7 +11,7 @@ import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_ import { localUIFilters, LocalUIFilterName } from './config'; import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; -export const getFilterAggregations = async ({ +export const getFilterAggregations = ({ indexPattern, uiFilters, projection, @@ -24,49 +24,44 @@ export const getFilterAggregations = async ({ }) => { const mappedFilters = localFilterNames.map(name => localUIFilters[name]); - const aggs = await Promise.all( - mappedFilters.map(async field => { - const filter = await getUiFiltersES( - indexPattern, - omit(uiFilters, field.name) - ); + const aggs = mappedFilters.map(field => { + const filter = getUiFiltersES(indexPattern, omit(uiFilters, field.name)); - const bucketCountAggregation = projection.body.aggs - ? { - aggs: { - bucket_count: { - cardinality: { - field: - projection.body.aggs[Object.keys(projection.body.aggs)[0]] - .terms.field - } + const bucketCountAggregation = projection.body.aggs + ? { + aggs: { + bucket_count: { + cardinality: { + field: + projection.body.aggs[Object.keys(projection.body.aggs)[0]] + .terms.field } } } - : {}; + } + : {}; - return { - [field.name]: { - filter: { - bool: { - filter - } - }, - aggs: { - by_terms: { - terms: { - field: field.fieldName, - order: { - _count: 'desc' as const - } - }, - ...bucketCountAggregation - } + return { + [field.name]: { + filter: { + bool: { + filter + } + }, + aggs: { + by_terms: { + terms: { + field: field.fieldName, + order: { + _count: 'desc' as const + } + }, + ...bucketCountAggregation } } - }; - }) - ); + } + }; + }); const mergedAggregations = Object.assign({}, ...aggs) as Partial< Record diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index ada41c3aa97c7..524e6ca640f3e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -33,7 +33,7 @@ export async function getLocalUIFilters({ delete projectionWithoutAggs.body.aggs; - const filterAggregations = await getFilterAggregations({ + const filterAggregations = getFilterAggregations({ indexPattern: dynamicIndexPattern, uiFilters, projection, diff --git a/x-pack/legacy/plugins/apm/server/new-platform/index.ts b/x-pack/legacy/plugins/apm/server/new-platform/index.ts deleted file mode 100644 index 8ad0cbbb811f3..0000000000000 --- a/x-pack/legacy/plugins/apm/server/new-platform/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(); -} diff --git a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts b/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts deleted file mode 100644 index e1cb1774469f2..0000000000000 --- a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts +++ /dev/null @@ -1,23 +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 { Server } from 'hapi'; -import { CoreSetup } from 'src/core/server'; -import { makeApmUsageCollector } from '../lib/apm_telemetry'; -import { createApmAgentConfigurationIndex } from '../lib/settings/agent_configuration/create_agent_config_index'; -import { createApmApi } from '../routes/create_apm_api'; - -export interface LegacySetup { - server: Server; -} - -export class Plugin { - public setup(core: CoreSetup, __LEGACY: LegacySetup) { - createApmApi().init(core, __LEGACY); - createApmAgentConfigurationIndex(core, __LEGACY); - makeApmUsageCollector(core, __LEGACY); - } -} diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts index 18fe547a34cf0..3a74c4377920a 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts @@ -5,25 +5,49 @@ */ import * as t from 'io-ts'; import { createApi } from './index'; -import { CoreSetup } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { Params } from '../typings'; -import { LegacySetup } from '../../new-platform/plugin'; - -const getCoreMock = () => (({} as unknown) as CoreSetup); +import { BehaviorSubject } from 'rxjs'; +import { APMConfig } from '../../../../../../plugins/apm/server'; +import { LegacySetup } from '../../../../../../plugins/apm/server/plugin'; + +const getCoreMock = () => { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const createRouter = jest.fn().mockReturnValue({ + get, + post, + put + }); -const getLegacyMock = () => - (({ - server: { - route: jest.fn() + const mock = {} as CoreSetup; + + return { + mock: { + ...mock, + http: { + ...mock.http, + createRouter + } + }, + get, + post, + put, + createRouter, + context: { + config$: new BehaviorSubject({} as APMConfig), + logger: ({ + error: jest.fn() + } as unknown) as Logger, + __LEGACY: {} as LegacySetup } - } as unknown) as LegacySetup & { - server: { route: ReturnType }; - }); + }; +}; describe('createApi', () => { it('registers a route with the server', () => { - const coreMock = getCoreMock(); - const legacySetupMock = getLegacyMock(); + const { mock, context, createRouter, post, get, put } = getCoreMock(); createApi() .add(() => ({ @@ -46,236 +70,249 @@ describe('createApi', () => { }, handler: async () => null })) - .init(coreMock, legacySetupMock); + .init(mock, context); - expect(legacySetupMock.server.route).toHaveBeenCalledTimes(3); + expect(createRouter).toHaveBeenCalledTimes(1); - const firstRoute = legacySetupMock.server.route.mock.calls[0][0]; + expect(get).toHaveBeenCalledTimes(1); + expect(post).toHaveBeenCalledTimes(1); + expect(put).toHaveBeenCalledTimes(1); - expect(firstRoute).toEqual({ - method: 'GET', + expect(get.mock.calls[0][0]).toEqual({ options: { tags: ['access:apm'] }, path: '/foo', - handler: expect.any(Function) + validate: expect.anything() }); - const secondRoute = legacySetupMock.server.route.mock.calls[1][0]; - - expect(secondRoute).toEqual({ - method: 'POST', + expect(post.mock.calls[0][0]).toEqual({ options: { tags: ['access:apm'] }, path: '/bar', - handler: expect.any(Function) + validate: expect.anything() }); - const thirdRoute = legacySetupMock.server.route.mock.calls[2][0]; - - expect(thirdRoute).toEqual({ - method: 'PUT', + expect(put.mock.calls[0][0]).toEqual({ options: { tags: ['access:apm', 'access:apm_write'] }, path: '/baz', - handler: expect.any(Function) + validate: expect.anything() }); }); describe('when validating', () => { const initApi = (params: Params) => { - const core = getCoreMock(); - const legacySetupMock = getLegacyMock(); - const handler = jest.fn(); + const { mock, context, createRouter, get, post } = getCoreMock(); + const handlerMock = jest.fn(); createApi() .add(() => ({ path: '/foo', params, - handler + handler: handlerMock })) - .init(core, legacySetupMock); - - const route = legacySetupMock.server.route.mock.calls[0][0]; - - const routeHandler = route.handler; + .init(mock, context); + + const routeHandler = get.mock.calls[0][1]; + const responseMock = { + ok: jest.fn(), + internalError: jest.fn(), + notFound: jest.fn(), + forbidden: jest.fn(), + badRequest: jest.fn() + }; - route.handler = (requestMock: any) => { - return routeHandler({ - // stub hapi's default values - params: {}, - query: {}, - payload: null, - ...requestMock - }); + const simulate = (requestMock: any) => { + return routeHandler( + {}, + { + // stub default values + params: {}, + query: {}, + body: {}, + ...requestMock + }, + responseMock + ); }; - return { route, handler }; + return { simulate, handlerMock, createRouter, get, post, responseMock }; }; - it('adds a _debug query parameter by default', () => { - const { handler, route } = initApi({}); + it('adds a _debug query parameter by default', async () => { + const { simulate, handlerMock, responseMock } = initApi({}); - expect(() => - route.handler({ - query: { - _debug: 'true' - } - }) - ).not.toThrow(); + await simulate({ query: { _debug: true } }); - expect(handler).toHaveBeenCalledTimes(1); + expect(handlerMock).toHaveBeenCalledTimes(1); - const params = handler.mock.calls[0][1]; + expect(responseMock.ok).toHaveBeenCalled(); - expect(params).toEqual({}); + expect(responseMock.badRequest).not.toHaveBeenCalled(); - expect(() => - route.handler({ - query: { - _debug: 1 - } - }) - ).toThrow(); + const params = handlerMock.mock.calls[0][0].context.params; + + expect(params).toEqual({ + query: { + _debug: true + } + }); + + await simulate({ + query: { + _debug: 1 + } + }); + + expect(responseMock.badRequest).toHaveBeenCalled(); }); - it('throws if any parameters are used but no types are defined', () => { - const { route } = initApi({}); + it('throws if any parameters are used but no types are defined', async () => { + const { simulate, responseMock } = initApi({}); - expect(() => - route.handler({ - query: { - _debug: 'true', - extra: '' - } - }) - ).toThrow(); + await simulate({ + query: { + _debug: true, + extra: '' + } + }); - expect(() => - route.handler({ - payload: { foo: 'bar' } - }) - ).toThrow(); + expect(responseMock.badRequest).toHaveBeenCalledTimes(1); - expect(() => - route.handler({ - params: { - foo: 'bar' - } - }) - ).toThrow(); - }); + await simulate({ + body: { foo: 'bar' } + }); - it('validates path parameters', () => { - const { handler, route } = initApi({ path: t.type({ foo: t.string }) }); + expect(responseMock.badRequest).toHaveBeenCalledTimes(2); - expect(() => - route.handler({ - params: { - foo: 'bar' - } + await simulate({ + params: { + foo: 'bar' + } + }); + + expect(responseMock.badRequest).toHaveBeenCalledTimes(3); + }); + + it('validates path parameters', async () => { + const { simulate, handlerMock, responseMock } = initApi({ + path: t.type({ + foo: t.string }) - ).not.toThrow(); + }); + + await simulate({ + params: { + foo: 'bar' + } + }); - expect(handler).toHaveBeenCalledTimes(1); + expect(handlerMock).toHaveBeenCalledTimes(1); - const params = handler.mock.calls[0][1]; + expect(responseMock.ok).toHaveBeenCalledTimes(1); + expect(responseMock.badRequest).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].context.params; expect(params).toEqual({ path: { foo: 'bar' + }, + query: { + _debug: false } }); - handler.mockClear(); + await simulate({ + params: { + bar: 'foo' + } + }); - expect(() => - route.handler({ - params: { - bar: 'foo' - } - }) - ).toThrow(); + expect(responseMock.badRequest).toHaveBeenCalledTimes(1); - expect(() => - route.handler({ - params: { - foo: 9 - } - }) - ).toThrow(); - - expect(() => - route.handler({ - params: { - foo: 'bar', - extra: '' - } - }) - ).toThrow(); + await simulate({ + params: { + foo: 9 + } + }); + + expect(responseMock.badRequest).toHaveBeenCalledTimes(2); + + await simulate({ + params: { + foo: 'bar', + extra: '' + } + }); + + expect(responseMock.badRequest).toHaveBeenCalledTimes(3); }); - it('validates body parameters', () => { - const { handler, route } = initApi({ body: t.string }); + it('validates body parameters', async () => { + const { simulate, handlerMock, responseMock } = initApi({ + body: t.string + }); - expect(() => - route.handler({ - payload: '' - }) - ).not.toThrow(); + await simulate({ + body: '' + }); - expect(handler).toHaveBeenCalledTimes(1); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(responseMock.ok).toHaveBeenCalledTimes(1); + expect(responseMock.badRequest).not.toHaveBeenCalled(); - const params = handler.mock.calls[0][1]; + const params = handlerMock.mock.calls[0][0].context.params; expect(params).toEqual({ - body: '' + body: '', + query: { + _debug: false + } }); - handler.mockClear(); + await simulate({ + body: null + }); - expect(() => - route.handler({ - payload: null - }) - ).toThrow(); + expect(responseMock.badRequest).toHaveBeenCalledTimes(1); }); - it('validates query parameters', () => { - const { handler, route } = initApi({ + it('validates query parameters', async () => { + const { simulate, handlerMock, responseMock } = initApi({ query: t.type({ bar: t.string }) }); - expect(() => - route.handler({ - query: { - bar: '', - _debug: 'true' - } - }) - ).not.toThrow(); + await simulate({ + query: { + bar: '', + _debug: true + } + }); - expect(handler).toHaveBeenCalledTimes(1); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(responseMock.ok).toHaveBeenCalledTimes(1); + expect(responseMock.badRequest).not.toHaveBeenCalled(); - const params = handler.mock.calls[0][1]; + const params = handlerMock.mock.calls[0][0].context.params; expect(params).toEqual({ query: { - bar: '' + bar: '', + _debug: true } }); - handler.mockClear(); + await simulate({ + query: { + bar: '', + foo: '' + } + }); - expect(() => - route.handler({ - query: { - bar: '', - foo: '' - } - }) - ).toThrow(); + expect(responseMock.badRequest).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts index 66f28a9bf6d44..2e97b01d0d108 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts @@ -3,13 +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 { merge, pick, omit, difference } from 'lodash'; +import { pick, difference } from 'lodash'; import Boom from 'boom'; -import { CoreSetup } from 'src/core/server'; -import { Request, ResponseToolkit } from 'hapi'; +import { schema } from '@kbn/config-schema'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; +import { KibanaResponseFactory } from 'src/core/server'; +import { APMConfig } from '../../../../../../plugins/apm/server'; import { ServerAPI, RouteFactoryFn, @@ -17,10 +18,8 @@ import { Route, Params } from '../typings'; -import { jsonRt } from '../../../common/runtime_types/json_rt'; -import { LegacySetup } from '../../new-platform/plugin'; -const debugRt = t.partial({ _debug: jsonRt.pipe(t.boolean) }); +const debugRt = t.partial({ _debug: t.boolean }); export function createApi() { const factoryFns: Array> = []; @@ -30,17 +29,32 @@ export function createApi() { factoryFns.push(fn); return this as any; }, - init(core: CoreSetup, __LEGACY: LegacySetup) { - const { server } = __LEGACY; + init(core, { config$, logger, __LEGACY }) { + const router = core.http.createRouter(); + + let config = {} as APMConfig; + + config$.subscribe(val => { + config = val; + }); + factoryFns.forEach(fn => { const { params = {}, + path, options = { tags: ['access:apm'] }, - ...route - } = fn(core, __LEGACY) as Route; + method, + handler + } = fn(core) as Route; + + const routerMethod = (method || 'GET').toLowerCase() as + | 'post' + | 'put' + | 'get' + | 'delete'; const bodyRt = params.body; - const fallbackBodyRt = bodyRt || t.null; + const fallbackBodyRt = bodyRt || t.strict({}); const rts = { // add _debug query parameter to all routes @@ -51,65 +65,80 @@ export function createApi() { body: bodyRt && 'props' in bodyRt ? t.exact(bodyRt) : fallbackBodyRt }; - server.route( - merge( - { - options, - method: 'GET' - }, - route, - { - handler: (request: Request, h: ResponseToolkit) => { - const paramMap = { - path: request.params, - body: request.payload, - query: request.query - }; + router[routerMethod]( + { + path, + options, + validate: { + ...(routerMethod === 'get' + ? {} + : { body: schema.object({}, { allowUnknowns: true }) }), + params: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { allowUnknowns: true }) + } + }, + async (context, request, response) => { + try { + const paramMap = { + path: request.params, + body: request.body, + query: { + _debug: false, + ...request.query + } + }; - const parsedParams = (Object.keys(rts) as Array< - keyof typeof rts - >).reduce((acc, key) => { - const codec = rts[key]; - const value = paramMap[key]; + const parsedParams = (Object.keys(rts) as Array< + keyof typeof rts + >).reduce((acc, key) => { + const codec = rts[key]; + const value = paramMap[key]; - const result = codec.decode(value); + const result = codec.decode(value); - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); + } - const strippedKeys = difference( - Object.keys(value || {}), - Object.keys(result.right || {}) + const strippedKeys = difference( + Object.keys(value || {}), + Object.keys(result.right || {}) + ); + + if (strippedKeys.length) { + throw Boom.badRequest( + `Unknown keys specified: ${strippedKeys}` ); + } + + const parsedValue = result.right; - if (strippedKeys.length) { - throw Boom.badRequest( - `Unknown keys specified: ${strippedKeys}` - ); - } - - // hide _debug from route handlers - const parsedValue = - key === 'query' - ? omit(result.right, '_debug') - : result.right; - - return { - ...acc, - [key]: parsedValue - }; - }, {} as Record); - - return route.handler( - request, + return { + ...acc, + [key]: parsedValue + }; + }, {} as Record); + + const data = await handler({ + request, + context: { + ...context, + __LEGACY, // only return values for parameters that have runtime types - pick(parsedParams, Object.keys(params)), - h - ); + params: pick(parsedParams, ...Object.keys(params), 'query'), + config, + logger + } + }); + + return response.ok({ body: data }); + } catch (error) { + if (Boom.isBoom(error)) { + return convertBoomToKibanaResponse(error, response); } + throw error; } - ) + } ); }); } @@ -117,3 +146,26 @@ export function createApi() { return api; } + +function convertBoomToKibanaResponse( + error: Boom, + response: KibanaResponseFactory +) { + const opts = { body: error.message }; + switch (error.output.statusCode) { + case 404: + return response.notFound(opts); + + case 400: + return response.badRequest(opts); + + case 403: + return response.forbidden(opts); + + default: + return response.custom({ + statusCode: error.output.statusCode, + ...opts + }); + } +} diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index c35b66b453634..1735aa9da7dca 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -42,7 +42,8 @@ import { transactionGroupsChartsRoute, transactionGroupsDistributionRoute, transactionGroupsRoute, - transactionGroupsAvgDurationByCountry + transactionGroupsAvgDurationByCountry, + transactionGroupsAvgDurationByBrowser } from './transaction_groups'; import { errorGroupsLocalFiltersRoute, @@ -102,6 +103,7 @@ const createApmApi = () => { .add(transactionGroupsChartsRoute) .add(transactionGroupsDistributionRoute) .add(transactionGroupsRoute) + .add(transactionGroupsAvgDurationByBrowser) .add(transactionGroupsAvgDurationByCountry) // UI filters diff --git a/x-pack/legacy/plugins/apm/server/routes/errors.ts b/x-pack/legacy/plugins/apm/server/routes/errors.ts index a315dd10023dc..0c363b6f8ee72 100644 --- a/x-pack/legacy/plugins/apm/server/routes/errors.ts +++ b/x-pack/legacy/plugins/apm/server/routes/errors.ts @@ -27,10 +27,11 @@ export const errorsRoute = createRoute(core => ({ rangeRt ]) }, - handler: async (req, { query, path }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { sortField, sortDirection } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { sortField, sortDirection } = params.query; return getErrorGroups({ serviceName, @@ -50,9 +51,9 @@ export const errorGroupsRoute = createRoute(() => ({ }), query: t.intersection([uiFiltersRt, rangeRt]) }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { serviceName, groupId } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName, groupId } = context.params.path; return getErrorGroup({ serviceName, groupId, setup }); } })); @@ -71,10 +72,11 @@ export const errorDistributionRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { groupId } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { groupId } = params.query; return getErrorDistribution({ serviceName, groupId, setup }); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts index 5e2b7a378f2bc..539846430c7f8 100644 --- a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts @@ -8,15 +8,15 @@ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_ind import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; -export const staticIndexPatternRoute = createRoute((core, { server }) => ({ +export const staticIndexPatternRoute = createRoute(() => ({ method: 'POST', path: '/api/apm/index_pattern/static', - handler: async (req, params, h) => { - const setup = await setupRequest(req); - await createStaticIndexPattern(setup, server); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + await createStaticIndexPattern(setup, context); // send empty response regardless of outcome - return h.response().code(204); + return undefined; } })); @@ -31,8 +31,8 @@ export const dynamicIndexPatternRoute = createRoute(() => ({ ]) }) }, - handler: async request => { - const { dynamicIndexPattern } = await setupRequest(request); + handler: async ({ context, request }) => { + const { dynamicIndexPattern } = await setupRequest(context, request); return { dynamicIndexPattern }; } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/metrics.ts b/x-pack/legacy/plugins/apm/server/routes/metrics.ts index ef9145b3dcd4a..74fa625af8802 100644 --- a/x-pack/legacy/plugins/apm/server/routes/metrics.ts +++ b/x-pack/legacy/plugins/apm/server/routes/metrics.ts @@ -27,10 +27,11 @@ export const metricsChartsRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { agentName, serviceNodeName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { agentName, serviceNodeName } = params.query; return await getMetricsChartDataByAgent({ setup, serviceName, diff --git a/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts b/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts index 285dd5b1f10f5..33ecbb316d415 100644 --- a/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts @@ -17,9 +17,10 @@ export const serviceNodesRoute = createRoute(core => ({ }), query: t.intersection([rangeRt, uiFiltersRt]) }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { serviceName } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; return getServiceNodes({ setup, diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index 4b955c7a6e981..91495bb96b032 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -5,6 +5,7 @@ */ import * as t from 'io-ts'; +import Boom from 'boom'; import { AgentName } from '../../typings/es_schemas/ui/fields/Agent'; import { createApmTelementry, @@ -19,13 +20,13 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/services/map'; -export const servicesRoute = createRoute((core, { server }) => ({ +export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', params: { query: t.intersection([uiFiltersRt, rangeRt]) }, - handler: async req => { - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); const services = await getServices(setup); // Store telemetry data derived from services @@ -33,7 +34,7 @@ export const servicesRoute = createRoute((core, { server }) => ({ ({ agentName }) => agentName as AgentName ); const apmTelemetry = createApmTelementry(agentNames); - storeApmServicesTelemetry(server, apmTelemetry); + storeApmServicesTelemetry(context.__LEGACY.server, apmTelemetry); return services; } @@ -47,9 +48,9 @@ export const serviceAgentNameRoute = createRoute(() => ({ }), query: rangeRt }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { serviceName } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; return getServiceAgentName(serviceName, setup); } })); @@ -62,9 +63,9 @@ export const serviceTransactionTypesRoute = createRoute(() => ({ }), query: rangeRt }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { serviceName } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; return getServiceTransactionTypes(serviceName, setup); } })); @@ -78,9 +79,9 @@ export const serviceNodeMetadataRoute = createRoute(() => ({ }), query: t.intersection([uiFiltersRt, rangeRt]) }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { serviceName, serviceNodeName } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName, serviceNodeName } = context.params.path; return getServiceNodeMetadata({ setup, serviceName, serviceNodeName }); } })); @@ -90,12 +91,10 @@ export const serviceMapRoute = createRoute(() => ({ params: { query: rangeRt }, - handler: async (request, _response, hapi) => { - const setup = await setupRequest(request); - if (setup.config.get('xpack.apm.serviceMapEnabled')) { + handler: async ({ context }) => { + if (context.config['xpack.apm.serviceMapEnabled']) { return getServiceMap(); - } else { - return hapi.response().code(404); } + return new Boom('Not found', { statusCode: 404 }); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts index 2867cef28d952..b897dfb4b9123 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts @@ -5,6 +5,7 @@ */ import * as t from 'io-ts'; +import Boom from 'boom'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration'; @@ -21,8 +22,8 @@ import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_ // get list of configurations export const agentConfigurationRoute = createRoute(core => ({ path: '/api/apm/settings/agent-configuration', - handler: async req => { - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); return await listConfigurations({ setup }); } })); @@ -39,9 +40,9 @@ export const deleteAgentConfigurationRoute = createRoute(() => ({ configurationId: t.string }) }, - handler: async (req, { path }) => { - const setup = await setupRequest(req); - const { configurationId } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { configurationId } = context.params.path; return await deleteConfiguration({ configurationId, setup @@ -53,8 +54,8 @@ export const deleteAgentConfigurationRoute = createRoute(() => ({ export const listAgentConfigurationServicesRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/agent-configuration/services', - handler: async req => { - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); return await getServiceNames({ setup }); @@ -84,9 +85,9 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ params: { query: t.partial({ serviceName: t.string }) }, - handler: async (req, { query }) => { - const setup = await setupRequest(req); - const { serviceName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.query; return await getEnvironments({ serviceName, setup }); } })); @@ -97,9 +98,9 @@ export const agentConfigurationAgentNameRoute = createRoute(() => ({ params: { query: t.type({ serviceName: t.string }) }, - handler: async (req, { query }) => { - const setup = await setupRequest(req); - const { serviceName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.query; const agentName = await getAgentNameByService({ serviceName, setup }); return agentName; } @@ -114,9 +115,12 @@ export const createAgentConfigurationRoute = createRoute(() => ({ options: { tags: ['access:apm', 'access:apm_write'] }, - handler: async (req, { body }) => { - const setup = await setupRequest(req); - return await createOrUpdateConfiguration({ configuration: body, setup }); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return await createOrUpdateConfiguration({ + configuration: context.params.body, + setup + }); } })); @@ -132,12 +136,12 @@ export const updateAgentConfigurationRoute = createRoute(() => ({ }), body: agentPayloadRt }, - handler: async (req, { path, body }) => { - const setup = await setupRequest(req); - const { configurationId } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { configurationId } = context.params.path; return await createOrUpdateConfiguration({ configurationId, - configuration: body, + configuration: context.params.body, setup }); } @@ -156,8 +160,9 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ etag: t.string }) }, - handler: async (req, { body }, h) => { - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { body } = context.params; const config = await searchConfigurations({ serviceName: body.service.name, environment: body.service.environment, @@ -165,7 +170,7 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ }); if (!config) { - return h.response().code(404); + throw new Boom('Not found', { statusCode: 404 }); } // update `applied_by_agent` field if etags match diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts index 4afcf135a1a76..b66eb05f6eda5 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts @@ -13,28 +13,20 @@ import { import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; // get list of apm indices and values -export const apmIndexSettingsRoute = createRoute((core, { server }) => ({ +export const apmIndexSettingsRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/apm-index-settings', - handler: async req => { - const config = server.config(); - const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( - req - ); - return await getApmIndexSettings({ config, savedObjectsClient }); + handler: async ({ context }) => { + return await getApmIndexSettings({ context }); } })); // get apm indices configuration object -export const apmIndicesRoute = createRoute((core, { server }) => ({ +export const apmIndicesRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/apm-indices', - handler: async req => { - const config = server.config(); - const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( - req - ); - return await getApmIndices({ config, savedObjectsClient }); + handler: async ({ context }) => { + return await getApmIndices(context); } })); @@ -49,14 +41,11 @@ export const saveApmIndicesRoute = createRoute(() => ({ 'apm_oss.onboardingIndices': t.string, 'apm_oss.spanIndices': t.string, 'apm_oss.transactionIndices': t.string, - 'apm_oss.metricsIndices': t.string, - 'apm_oss.apmAgentConfigurationIndex': t.string + 'apm_oss.metricsIndices': t.string }) }, - handler: async (req, { body }) => { - const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( - req - ); - return await saveApmIndices(savedObjectsClient, body); + handler: async ({ context, request }) => { + const { body } = context.params; + return await saveApmIndices(context, body); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/traces.ts b/x-pack/legacy/plugins/apm/server/routes/traces.ts index cd2c86a5c9ca3..089408a03afe9 100644 --- a/x-pack/legacy/plugins/apm/server/routes/traces.ts +++ b/x-pack/legacy/plugins/apm/server/routes/traces.ts @@ -16,8 +16,8 @@ export const tracesRoute = createRoute(() => ({ params: { query: t.intersection([rangeRt, uiFiltersRt]) }, - handler: async req => { - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); return getTransactionGroupList({ type: 'top_traces' }, setup); } })); @@ -30,9 +30,8 @@ export const tracesByIdRoute = createRoute(() => ({ }), query: rangeRt }, - handler: async (req, { path }) => { - const { traceId } = path; - const setup = await setupRequest(req); - return getTrace(traceId, setup); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return getTrace(context.params.path.traceId, setup); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts index 0b5c29fc29857..2170a8fbb9692 100644 --- a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts @@ -12,6 +12,7 @@ import { getTransactionBreakdown } from '../lib/transactions/breakdown'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; +import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser'; import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country'; export const transactionGroupsRoute = createRoute(() => ({ @@ -28,10 +29,10 @@ export const transactionGroupsRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const { serviceName } = path; - const { transactionType } = query; - const setup = await setupRequest(req); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionType } = context.params.query; return getTransactionGroupList( { @@ -59,10 +60,10 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { transactionType, transactionName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionType, transactionName } = context.params.query; return getTransactionCharts({ serviceName, @@ -92,15 +93,15 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; const { transactionType, transactionName, transactionId = '', traceId = '' - } = query; + } = context.params.query; return getTransactionDistribution({ serviceName, @@ -130,10 +131,10 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { transactionName, transactionType } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionName, transactionType } = context.params.query; return getTransactionBreakdown({ serviceName, @@ -144,6 +145,32 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({ } })); +export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({ + path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser`, + params: { + path: t.type({ + serviceName: t.string + }), + query: t.intersection([ + t.partial({ + transactionType: t.string, + transactionName: t.string + }), + uiFiltersRt, + rangeRt + ]) + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + + return getTransactionAvgDurationByBrowser({ + serviceName, + setup + }); + } +})); + export const transactionGroupsAvgDurationByCountry = createRoute(() => ({ path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country`, params: { @@ -156,10 +183,10 @@ export const transactionGroupsAvgDurationByCountry = createRoute(() => ({ t.partial({ transactionName: t.string }) ]) }, - handler: async (req, { path, query }) => { - const setup = await setupRequest(req); - const { serviceName } = path; - const { transactionName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionName } = context.params.query; return getTransactionAvgDurationByCountry({ serviceName, diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 77d96d3677494..9b114eba72626 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -5,11 +5,17 @@ */ import t from 'io-ts'; -import { Request, ResponseToolkit } from 'hapi'; -import { CoreSetup } from 'src/core/server'; +import { + CoreSetup, + KibanaRequest, + RequestHandlerContext, + Logger +} from 'src/core/server'; import { PickByValue, Optional } from 'utility-types'; +import { Observable } from 'rxjs'; +import { Server } from 'hapi'; import { FetchOptions } from '../../public/services/rest/callApi'; -import { LegacySetup } from '../new-platform/plugin'; +import { APMConfig } from '../../../../../plugins/apm/server'; export interface Params { query?: t.HasProps; @@ -37,22 +43,35 @@ export interface Route< options?: { tags: Array<'access:apm' | 'access:apm_write'>; }; - handler: ( - req: Request, - params: DecodeParams, - h: ResponseToolkit - ) => Promise; + handler: (kibanaContext: { + context: APMRequestHandlerContext>; + request: KibanaRequest; + }) => Promise; } +export type APMLegacyServer = Pick & { + plugins: { + elasticsearch: Server['plugins']['elasticsearch']; + }; +}; + +export type APMRequestHandlerContext< + TDecodedParams extends { [key in keyof Params]: any } = {} +> = RequestHandlerContext & { + params: { query: { _debug: boolean } } & TDecodedParams; + config: APMConfig; + logger: Logger; + __LEGACY: { + server: APMLegacyServer; + }; +}; + export type RouteFactoryFn< TPath extends string, TMethod extends HttpMethod | undefined, TParams extends Params, TReturn -> = ( - core: CoreSetup, - __LEGACY: LegacySetup -) => Route; +> = (core: CoreSetup) => Route; export interface RouteState { [key: string]: { @@ -83,7 +102,14 @@ export interface ServerAPI { }; } >; - init: (core: CoreSetup, __LEGACY: LegacySetup) => void; + init: ( + core: CoreSetup, + context: { + config$: Observable; + logger: Logger; + __LEGACY: { server: Server }; + } + ) => void; } // without this, TS does not recognize possible existence of `params` in `options` below diff --git a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts index d04b49cfe921d..dcca41c7e00df 100644 --- a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts @@ -6,7 +6,12 @@ import * as t from 'io-ts'; import { omit } from 'lodash'; -import { setupRequest, Setup } from '../lib/helpers/setup_request'; +import { + setupRequest, + Setup, + SetupUIFilters, + SetupTimeRange +} from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; import { Projection } from '../../common/projections/typings'; import { @@ -35,9 +40,9 @@ export const uiFiltersEnvironmentsRoute = createRoute(() => ({ rangeRt ]) }, - handler: async (req, { query }) => { - const setup = await setupRequest(req); - const { serviceName } = query; + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.query; return getEnvironments(setup, serviceName); } })); @@ -76,19 +81,19 @@ function createLocalFiltersRoute< return createRoute(() => ({ path, params: { - query: queryRt - ? t.intersection([queryRt, localUiBaseQueryRt]) - : localUiBaseQueryRt + query: t.intersection([localUiBaseQueryRt, queryRt]) }, - handler: async (request, { query }: { query: t.TypeOf }) => { - const setup = await setupRequest(request); + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { query } = context.params; + const { uiFilters, filterNames } = query; const parsedUiFilters = JSON.parse(uiFilters); const projection = getProjection({ query, setup: { ...setup, - uiFiltersES: await getUiFiltersES( + uiFiltersES: getUiFiltersES( setup.dynamicIndexPattern, omit(parsedUiFilters, filterNames) ) @@ -222,5 +227,5 @@ type GetProjection< setup }: { query: t.TypeOf; - setup: Setup; + setup: Setup & SetupUIFilters & SetupTimeRange; }) => TProjection; diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx index 3ac2ff72c0116..1f49711ae5e4a 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx @@ -265,7 +265,7 @@ const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ selectedIndex: null, }); -const FixedEuiFieldSearch: React.SFC & +const FixedEuiFieldSearch: React.FC & EuiFieldSearchProps & { inputRef?: (element: HTMLInputElement | null) => void; onSearch: (value: string) => void; @@ -275,10 +275,10 @@ const AutocompleteContainer = styled.div` position: relative; `; -const SuggestionsPanel = styled(EuiPanel).attrs({ +const SuggestionsPanel = styled(EuiPanel).attrs(() => ({ paddingSize: 'none', hasShadow: true, -})` +}))` position: absolute; width: 100%; margin-top: 2px; diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx index 2ae475475829f..a753a944a2ecb 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx @@ -18,7 +18,7 @@ interface SuggestionItemProps { suggestion: AutocompleteSuggestion; } -export const SuggestionItem: React.SFC = props => { +export const SuggestionItem: React.FC = props => { const { isSelected, onClick, onMouseEnter, suggestion } = props; return ( @@ -57,7 +57,7 @@ const SuggestionItemField = styled.div` padding: ${props => props.theme.eui.default.euiSizeXs}; `; -const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>` +const SuggestionItemIconField = styled(SuggestionItemField)<{ suggestionType: string }>` background-color: ${props => { return tint(0.1, getEuiIconColor(props.theme, props.suggestionType)); }}; @@ -69,12 +69,12 @@ const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: str width: ${props => props.theme.eui.default.euiSizeXl}; `; -const SuggestionItemTextField = SuggestionItemField.extend` +const SuggestionItemTextField = styled(SuggestionItemField)` flex: 2 0 0; font-family: ${props => props.theme.eui.default.euiCodeFontFamily}; `; -const SuggestionItemDescriptionField = SuggestionItemField.extend` +const SuggestionItemDescriptionField = styled(SuggestionItemField)` flex: 3 0 0; p { display: inline; diff --git a/x-pack/legacy/plugins/beats_management/public/components/config_list.tsx b/x-pack/legacy/plugins/beats_management/public/components/config_list.tsx index f860188e66800..3ffd42c5201f9 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/config_list.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/config_list.tsx @@ -28,7 +28,7 @@ const pagination = { hidePerPageOptions: true, }; -const ConfigListUi: React.SFC = props => ( +const ConfigListUi: React.FC = props => ( ; +const FixedSelect = EuiSelect as React.FC; interface ComponentProps extends FormsyInputProps, CommonProps { instantValidation: boolean; diff --git a/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx b/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx index 8d4c7703dfc4d..e525ea4be46e0 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx @@ -14,7 +14,7 @@ interface LayoutProps { modalClosePath?: string; } -export const NoDataLayout: React.SFC = withRouter( +export const NoDataLayout: React.FC = withRouter( ({ actionSection, title, modalClosePath, children, history }) => { return ( diff --git a/x-pack/legacy/plugins/beats_management/public/components/layouts/walkthrough.tsx b/x-pack/legacy/plugins/beats_management/public/components/layouts/walkthrough.tsx index 0a63ccdc87239..bcedfc063b0e5 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/layouts/walkthrough.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/layouts/walkthrough.tsx @@ -24,7 +24,7 @@ interface LayoutProps { activePath: string; } -export const WalkthroughLayout: React.SFC = ({ +export const WalkthroughLayout: React.FC = ({ walkthroughSteps, title, activePath, diff --git a/x-pack/legacy/plugins/beats_management/public/components/loading.tsx b/x-pack/legacy/plugins/beats_management/public/components/loading.tsx index f1c2455ec85b9..625ecef3fa3d0 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/loading.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/loading.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import * as React from 'react'; -export const Loading: React.SFC<{}> = () => ( +export const Loading: React.FC<{}> = () => ( diff --git a/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx index 7948501f5f873..efe5eb25a0ac1 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx @@ -48,7 +48,7 @@ interface BreadcrumbProps extends RouteProps { parentBreadcrumbs?: BreadcrumbData[]; } -export const Breadcrumb: React.SFC = ({ title, path, parentBreadcrumbs }) => ( +export const Breadcrumb: React.FC = ({ title, path, parentBreadcrumbs }) => ( {context => ( = (props: ComponentProps) => { +export const OptionControl: React.FC = (props: ComponentProps) => { switch (props.type) { case ActionComponentType.Action: if (!props.action) { diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx index 0802b4d8ea7e8..29581508d2ad5 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx @@ -90,7 +90,7 @@ export const WithURLState = withRouter(WithURLStateComponent); export function withUrlState( UnwrappedComponent: React.ComponentType -): React.SFC { +): React.FC { return (origProps: OP) => { return ( diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 526728bd77cac..83c610800b89b 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; import { setup as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; const getAutocompleteProvider = (language: string) => @@ -20,7 +19,7 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { public isKueryValid(kuery: string): boolean { try { - fromKueryExpression(kuery); + esKuery.fromKueryExpression(kuery); } catch (err) { return false; } @@ -31,9 +30,9 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { if (!this.isKueryValid(kuery)) { return ''; } - const ast = fromKueryExpression(kuery); + const ast = esKuery.fromKueryExpression(kuery); const indexPattern = await this.getIndexPattern(); - return JSON.stringify(toElasticsearchQuery(ast, indexPattern)); + return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } public async getSuggestions( kuery: string, diff --git a/x-pack/legacy/plugins/beats_management/public/pages/walkthrough/initial/index.tsx b/x-pack/legacy/plugins/beats_management/public/pages/walkthrough/initial/index.tsx index 26f2aa80de763..a78bf3fa7cf8e 100644 --- a/x-pack/legacy/plugins/beats_management/public/pages/walkthrough/initial/index.tsx +++ b/x-pack/legacy/plugins/beats_management/public/pages/walkthrough/initial/index.tsx @@ -17,7 +17,7 @@ type Props = AppPageProps & { intl: InjectedIntl; }; -const InitialWalkthroughPageComponent: React.SFC = props => { +const InitialWalkthroughPageComponent: React.FC = props => { if (props.location.pathname === '/walkthrough/initial') { return ( ; + import React, { FC } from 'react'; + let Formsy: FC; export interface FormsyInputProps { getErrorMessage(): any; getValue(): any; @@ -43,6 +43,6 @@ declare module 'formsy-react' { // function withFormsy( // component: // | React.Component - // | SFC + // | FC // ): React.Component; } diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js index ed83dbfcb75b7..141beb3d34d78 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js @@ -29,12 +29,6 @@ export class Plugin { has: key => has(config, key), }), route: def => this.routes.push(def), - usage: { - collectorSet: { - makeUsageCollector: () => {}, - register: () => {}, - }, - }, }; const { init } = this.props; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index d7ebbd87c97e6..271fc7a979057 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [ { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, { ...BaseElement, expression: 'image | render' }, ]; + +export const workpadWithGroupAsElement: CanvasWorkpad = { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'image | render' }, + { ...BaseElement, id: 'group-1234' }, + ], + }, + ], +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index 5b9f6f00940f4..c898db7467b44 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -531,7 +531,7 @@ export const ComponentStrings = { }), getKeyboardShortcutsLinkLabel: () => i18n.translate('xpack.canvas.helpMenu.keyboardShortcutsLinkLabel', { - defaultMessage: 'Keyboard Shortcuts', + defaultMessage: 'Keyboard shortcuts', }), }, KeyboardShortcutsDoc: { @@ -547,7 +547,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle', { - defaultMessage: 'Keyboard Shortcuts', + defaultMessage: 'Keyboard shortcuts', }), }, Link: { diff --git a/x-pack/legacy/plugins/canvas/public/app.js b/x-pack/legacy/plugins/canvas/public/app.js index 0ba7385cf7a9e..760bb7a46f955 100644 --- a/x-pack/legacy/plugins/canvas/public/app.js +++ b/x-pack/legacy/plugins/canvas/public/app.js @@ -9,7 +9,9 @@ import './angular/config'; import './angular/services'; import React from 'react'; import ReactDOM from 'react-dom'; +import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; +import { documentationLinks } from './lib/documentation_links'; import { CanvasRootController } from './angular/controllers'; // Import the uiExports that the application uses @@ -19,7 +21,6 @@ import 'uiExports/visRequestHandlers'; import 'uiExports/visEditorTypes'; import 'uiExports/savedObjectTypes'; import 'uiExports/spyModes'; -import 'uiExports/fieldFormats'; import 'uiExports/embeddableFactories'; import 'uiExports/interpreter'; @@ -34,6 +35,17 @@ import { HelpMenu } from './components/help_menu/help_menu'; chrome.setRootController('canvas', CanvasRootController); // add Canvas docs to help menu in global nav -chrome.helpExtension.set(domNode => { - ReactDOM.render(, domNode); +chrome.helpExtension.set({ + appName: i18n.translate('xpack.canvas.helpMenu.appName', { + defaultMessage: 'Canvas', + }), + links: [ + { + linkType: 'documentation', + href: documentationLinks.canvas, + }, + ], + content: domNode => { + ReactDOM.render(, domNode); + }, }); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx index 720f1726c1e3c..39ff3f45d602a 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -80,10 +80,10 @@ function areAllElementsInResolvedArgs(workpad: Workpad, resolvedArgs: ResolvedAr export const withUnconnectedElementsLoadedTelemetry = function

( Component: React.ComponentType

, trackMetric = trackCanvasUiMetric -): React.SFC

{ +): React.FC

{ return function ElementsLoadedTelemetry( props: P & ElementsLoadedTelemetryProps - ): React.SFCElement

{ + ): React.FunctionComponentElement

{ const { telemetryElementCounts, workpad, telemetryResolvedArgs, ...other } = props; const [currentWorkpadId, setWorkpadId] = useState(undefined); diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot index 134a3da5ec657..a42360d815176 100644 --- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot @@ -246,8 +246,9 @@ Array [ />

- - - +
@@ -277,8 +278,8 @@ Array [ best element ever

-
- +
@@ -599,8 +600,9 @@ Array [ />
- - - +
@@ -626,8 +628,8 @@ Array [

-
- +
@@ -949,8 +951,9 @@ Array [ />
- - - +
My Chart @@ -980,8 +983,8 @@ Array [

-
- +
@@ -1301,8 +1304,9 @@ Array [ />
- - - +
@@ -1332,8 +1336,8 @@ Array [

-
- +
diff --git a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot index 35135b5662b24..f09921607ef46 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot @@ -8,11 +8,11 @@ exports[`Storyshots components/Elements/ElementCard with click handler 1`] = ` } } > -
- - +
- +
`; @@ -60,8 +66,9 @@ exports[`Storyshots components/Elements/ElementCard with image 1`] = ` >
- - - +
Element 1 @@ -87,8 +94,8 @@ exports[`Storyshots components/Elements/ElementCard with image 1`] = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.

-
- +
@@ -103,11 +110,11 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = ` } } > -
- - +
- - +
+
`; @@ -270,8 +283,9 @@ exports[`Storyshots components/Elements/ElementCard with title and description 1 >
- - - +
Element 1 @@ -301,8 +315,8 @@ exports[`Storyshots components/Elements/ElementCard with title and description 1 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.

-
- +
diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot index c192e082523cb..01774f849dfe7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot @@ -14,11 +14,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = `
-
- - +
- +
@@ -115,11 +121,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = `
-
- - +
- +
@@ -216,11 +228,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = `
-
- - +
- +
@@ -332,11 +350,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`]
-
- - +
- +
@@ -448,11 +472,11 @@ exports[`Storyshots components/Elements/ElementGrid with tags filter 1`] = `
-
- - +
- - +
+
@@ -522,11 +552,11 @@ exports[`Storyshots components/Elements/ElementGrid with text filter 1`] = `
-
- - +
- - +
+
@@ -596,11 +632,11 @@ exports[`Storyshots components/Elements/ElementGrid without controls 1`] = `
-
- - +
- - +
+
-
- - +
- - +
+
-
- - +
- - +
+
diff --git a/x-pack/legacy/plugins/canvas/public/components/help_menu/help_menu.js b/x-pack/legacy/plugins/canvas/public/components/help_menu/help_menu.js index 6e1de129e84c6..4512ce2b4992e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/help_menu/help_menu.js +++ b/x-pack/legacy/plugins/canvas/public/components/help_menu/help_menu.js @@ -5,8 +5,7 @@ */ import React, { Fragment, PureComponent } from 'react'; -import { EuiButton, EuiHorizontalRule, EuiText, EuiSpacer, EuiPortal } from '@elastic/eui'; -import { documentationLinks } from '../../lib/documentation_links'; +import { EuiButtonEmpty, EuiPortal } from '@elastic/eui'; import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc'; import { ComponentStrings } from '../../../i18n'; @@ -26,19 +25,14 @@ export class HelpMenu extends PureComponent { render() { return ( - - - -

{strings.getHelpMenuDescription()}

-
- - - {strings.getDocumentationLinkLabel()} - - - + {strings.getKeyboardShortcutsLinkLabel()} - + {this.state.isFlyoutVisible && ( diff --git a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot index cf37481d8ad7c..e02d64e3e0647 100644 --- a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot @@ -1,1601 +1,1599 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` - +
-
-
+
+ } + tabIndex={1} + /> +
+
- -
-

- Keyboard Shortcuts -

-
+ Keyboard shortcuts + +
+
-
-

- Element controls -

-
-
-
- Cut -
-
- - - - CTRL - - - - - - X - - - -
-
- Copy -
-
- - - - CTRL - - - - - - C - - - -
-
- Paste -
-
- - - - CTRL - - - - - - V - - - -
-
- Clone -
-
- - - - CTRL - - - - - - D - - - -
-
- Delete -
-
- - - - DEL - - - - - - or - - - - - - BACKSPACE - - - -
-
- Bring forward -
-
- - - - CTRL - - - - - - ↑ - - - -
-
- Bring to front -
-
- - - - CTRL - - - - - - SHIFT - - - - - - ↑ - - - -
-
- Send backward -
-
- - - - CTRL - - - - - - ↓ - - - -
-
- Send to back -
-
- - - - CTRL - - - - - - SHIFT - - - - - - ↓ - - - -
-
- Group -
-
- - - - G - - - -
-
- Ungroup -
-
- - - - U - - - -
-
- Shift up by 10px -
-
- - - - ↑ - - - -
-
- Shift down by 10px -
-
- - - - ↓ - - - -
-
- Shift left by 10px -
-
- - - - ← - - - -
-
- Shift right by 10px -
-
- - - - → - - - -
-
- Shift up by 1px -
-
- - - - SHIFT - - - - - - ↑ - - - -
-
- Shift down by 1px -
-
- - - - SHIFT - - - - - - ↓ - - - -
-
- Shift left by 1px -
-
- - - - SHIFT - - - - - - ← - - - -
-
- Shift right by 1px -
-
- - - - SHIFT - - - - - - → - - - -
-
-
-
+ Element controls + +
+
+
+ Cut +
+
+ + + + CTRL + + + + + + X + + + +
+
+ Copy +
+
+ + + + CTRL + + + + + + C + + + +
+
+ Paste +
+
+ + + + CTRL + + + + + + V + + + +
+
+ Clone +
+
+ + + + CTRL + + + + + + D + + + +
+
+ Delete +
+
+ + + + DEL + + + + + + or + + + + + + BACKSPACE + + + +
+
+ Bring forward +
+
+ + + + CTRL + + + + + + ↑ + + + +
+
+ Bring to front +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + ↑ + + + +
+
+ Send backward +
+
+ + + + CTRL + + + + + + ↓ + + + +
+
+ Send to back +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + ↓ + + + +
+
+ Group +
+
+ + + + G + + + +
+
+ Ungroup +
+
+ + + + U + + + +
+
+ Shift up by 10px +
+
+ + + + ↑ + + + +
+
+ Shift down by 10px +
+
+ + + + ↓ + + + +
+
+ Shift left by 10px +
+
+ + + + ← + + + +
+
+ Shift right by 10px +
+
+ + + + → + + + +
+
+ Shift up by 1px +
+
+ + + + SHIFT + + + + + + ↑ + + + +
+
+ Shift down by 1px +
+
+ + + + SHIFT + + + + + + ↓ + + + +
+
+ Shift left by 1px +
+
+ + + + SHIFT + + + + + + ← + + + +
+
+ Shift right by 1px +
+
+ + + + SHIFT + + + + + + → + + + +
+
+
+
+

+ Expression controls +

+
+
-

- Expression controls -

-
-
-
- Run whole expression -
-
- - - - CTRL - - - - - - ENTER - - - -
-
-
-
+
+ Run whole expression +
+
+ + + + CTRL + + + + + + ENTER + + + +
+
+
+
+

+ Editor controls +

+
+
-

- Editor controls -

-
-
-
- Select multiple elements -
-
- - - - SHIFT - - - - - - CLICK - - - -
-
- Resize from center -
-
- - - - ALT - - - - - - DRAG - - - -
-
- Move, resize, and rotate without snapping -
-
- - - - CTRL - - - - - - DRAG - - - -
-
- Select element below -
-
- - - - CTRL - - - - - - CLICK - - - -
-
- Undo last action -
-
- - - - CTRL - - - - - - Z - - - -
-
- Redo last action -
-
- - - - CTRL - - - - - - SHIFT - - - - - - Z - - - -
-
- Go to previous page -
-
- - - - ALT - - - - - - [ - - - -
-
- Go to next page -
-
- - - - ALT - - - - - - ] - - - -
-
- Toggle edit mode -
-
- - - - ALT - - - - - - E - - - -
-
- Show grid -
-
- - - - ALT - - - - - - G - - - -
-
- Refresh workpad -
-
- - - - ALT - - - - - - R - - - -
-
- Zoom in -
-
- - - - CTRL - - - - - - ALT - - - - - - + - - - -
-
- Zoom out -
-
- - - - CTRL - - - - - - ALT - - - - - - - - - - -
-
- Reset zoom to 100% -
-
- - - - CTRL - - - - - - ALT - - - - - - [ - - - -
-
- Enter presentation mode -
-
- - - - ALT - - - - - - F - - - - - - or - - - - - - ALT - - - - - - P - - - -
-
-
-
+
+ Select multiple elements +
+
+ + + + SHIFT + + + + + + CLICK + + + +
+
+ Resize from center +
+
+ + + + ALT + + + + + + DRAG + + + +
+
+ Move, resize, and rotate without snapping +
+
+ + + + CTRL + + + + + + DRAG + + + +
+
+ Select element below +
+
+ + + + CTRL + + + + + + CLICK + + + +
+
+ Undo last action +
+
+ + + + CTRL + + + + + + Z + + + +
+
+ Redo last action +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + Z + + + +
+
+ Go to previous page +
+
+ + + + ALT + + + + + + [ + + + +
+
+ Go to next page +
+
+ + + + ALT + + + + + + ] + + + +
+
+ Toggle edit mode +
+
+ + + + ALT + + + + + + E + + + +
+
+ Show grid +
+
+ + + + ALT + + + + + + G + + + +
+
+ Refresh workpad +
+
+ + + + ALT + + + + + + R + + + +
+
+ Zoom in +
+
+ + + + CTRL + + + + + + ALT + + + + + + + + + + +
+
+ Zoom out +
+
+ + + + CTRL + + + + + + ALT + + + + + + - + + + +
+
+ Reset zoom to 100% +
+
+ + + + CTRL + + + + + + ALT + + + + + + [ + + + +
+
+ Enter presentation mode +
+
+ + + + ALT + + + + + + F + + + + + + or + + + + + + ALT + + + + + + P + + + +
+
+
+
+

+ Presentation controls +

+
+
-

- Presentation controls -

-
-
-
- Enter presentation mode -
-
- - - - ALT - - - - - - F - - - - - - or - - - - - - ALT - - - - - - P - - - -
-
- Exit presentation mode -
-
- - - - ESC - - - -
-
- Go to previous page -
-
- - - - ALT - - - - - - [ - - - - - - or - - - - - - BACKSPACE - - - - - - or - - - - - - ← - - - -
-
- Go to next page -
-
- - - - ALT - - - - - - ] - - - - - - or - - - - - - SPACE - - - - - - or - - - - - - → - - - -
-
- Refresh workpad -
-
- - - - ALT - - - - - - R - - - -
-
- Toggle page cycling -
-
- - - - P - - - -
-
-
-
+
+ Enter presentation mode +
+
+ + + + ALT + + + + + + F + + + + + + or + + + + + + ALT + + + + + + P + + + +
+
+ Exit presentation mode +
+
+ + + + ESC + + + +
+
+ Go to previous page +
+
+ + + + ALT + + + + + + [ + + + + + + or + + + + + + BACKSPACE + + + + + + or + + + + + + ← + + + +
+
+ Go to next page +
+
+ + + + ALT + + + + + + ] + + + + + + or + + + + + + SPACE + + + + + + or + + + + + + → + + + +
+
+ Refresh workpad +
+
+ + + + ALT + + + + + + R + + + +
+
+ Toggle page cycling +
+
+ + + + P + + + +
+
+
-
- +
+
`; diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index 33067f1837f41..f1ed069c15d4d 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -29,6 +29,7 @@ export function get(workpadId) { }); } +// TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { return fetch.put(`${apiPath}/${id}`, workpad); } diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index 888d9a5f36c32..b338971103381 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -61,7 +61,7 @@ export class Plugin { }, }); - registerCanvasUsageCollector(core, plugins); + registerCanvasUsageCollector(plugins.usageCollection, core); loadSampleData( plugins.sampleData.addSavedObjectsToSampleDataset, plugins.sampleData.addAppLinksToSampleDataset diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index a0502c5e891a2..515d5b5e895ed 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; @@ -13,6 +12,5 @@ import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); - workpad(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js b/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js deleted file mode 100644 index 09a5c3b89c31e..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { workpad } from './workpad'; - -const routePrefix = API_ROUTE_WORKPAD; -const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; -const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - -jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); - -describe(`${CANVAS_TYPE} API`, () => { - const savedObjectsClient = { - get: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - }; - - afterEach(() => { - savedObjectsClient.get.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.find.mockReset(); - }); - - // Mock toISOString function of all Date types - global.Date = class Date extends global.Date { - toISOString() { - return '2019-02-12T21:01:22.479Z'; - } - }; - - // Setup mock server - const mockServer = new Hapi.Server({ debug: false, port: 0 }); - const mockEs = { - getCluster: () => ({ - errors: { - // formatResponse will fail without objects here - '400': Error, - '401': Error, - '403': Error, - '404': Error, - }, - }), - }; - - mockServer.ext('onRequest', (req, h) => { - req.getSavedObjectsClient = () => savedObjectsClient; - return h.continue; - }); - workpad(mockServer.route.bind(mockServer), mockEs); - - describe(`GET ${routePrefix}/{id}`, () => { - test('returns successful response', async () => { - const request = { - method: 'GET', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "foo": true, - "id": "123", -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - describe(`POST ${routePrefix}`, () => { - test('returns successful response without id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "workpad-123abc", - }, - ], -] -`); - }); - - test('returns succesful response with id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - id: '123', - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'PUT', - url: `${routePrefix}/123`, - payload: { - id: '234', - foo: true, - }, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`DELETE ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'DELETE', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.delete.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - it(`GET ${routePrefix}/find`, async () => { - const request = { - method: 'GET', - url: `${routePrefix}/find?name=abc&page=2&perPage=10`, - }; - - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - attributes: { - foo: true, - }, - }, - ], - }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "workpads": Array [ - Object { - "foo": true, - "id": "1", - }, - ], -} -`); - expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - Object { - "fields": Array [ - "id", - "name", - "@created", - "@timestamp", - ], - "page": "2", - "perPage": "10", - "search": "abc* | abc", - "searchFields": Array [ - "name", - ], - "sortField": "@timestamp", - "sortOrder": "desc", - "type": "canvas-workpad", - }, - ], -] -`); - }); - - describe(`PUT ${routePrefixAssets}/{id}`, () => { - test('only updates assets', async () => { - const request = { - method: 'PUT', - url: `${routePrefixAssets}/123`, - payload: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - 'asset-456': { - id: 'asset-456', - '@created': '2019-02-15T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }; - - // provide some existing workpad data to check that it's preserved - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - "asset-456": Object { - "@created": "2019-02-15T00:00:00.000Z", - "id": "asset-456", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "name": "fake workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefixStructures}/{id}`, () => { - test('only updates workpad', async () => { - const request = { - method: 'PUT', - url: `${routePrefixStructures}/123`, - payload: { - name: 'renamed workpad', - css: '.canvasPage { color: LavenderBlush; }', - }, - }; - - // provide some existing asset data and a name to replace - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - assets: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "css": ".canvasPage { color: LavenderBlush; }", - "name": "renamed workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts b/x-pack/legacy/plugins/canvas/server/routes/workpad.ts deleted file mode 100644 index 380fe97ca9ef1..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts +++ /dev/null @@ -1,254 +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 boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -import { CoreSetup } from '../shim'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CanvasWorkpad } from '../../types'; - -type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface WorkpadRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type WorkpadRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad; -}; - -type FindWorkpadRequest = WorkpadRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -type AssetsRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad['assets']; -}; - -export function workpad( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore EsErrors is not on the Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - const routePrefix = API_ROUTE_WORKPAD; - const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; - const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - const formatResponse = formatRes(esErrors); - - function createWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A workpad payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('workpad') } - ); - } - - function updateWorkpad( - req: WorkpadRequest | AssetsRequest, - newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] } - ) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpadObject => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...(workpadObject.attributes as SavedObjectAttributes), - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - } - - function deleteWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CANVAS_TYPE, id); - } - - function findWorkpad(req: FindWorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CANVAS_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', '@created', '@timestamp'], - page, - perPage, - }); - } - - // get workpad - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient - .get(CANVAS_TYPE, id) - .then(obj => { - if ( - // not sure if we need to be this defensive - obj.type === 'canvas-workpad' && - obj.attributes && - obj.attributes.pages && - obj.attributes.pages.length - ) { - obj.attributes.pages.forEach(page => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } - return obj; - }) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse); - }, - }); - - // create workpad - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return createWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad assets - route({ - method: 'PUT', - path: `${routePrefixAssets}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: AssetsRequest) { - const payload = { assets: request.payload }; - return updateWorkpad(request, payload) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad structures - route({ - method: 'PUT', - path: `${routePrefixStructures}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // delete workpad - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler(request: WorkpadRequest) { - return deleteWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // find workpads - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler(request: FindWorkpadRequest) { - return findWorkpad(request) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - workpads: [], - }; - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/canvas/server/shim.ts b/x-pack/legacy/plugins/canvas/server/shim.ts index c043f268af8ea..7641e51f14e56 100644 --- a/x-pack/legacy/plugins/canvas/server/shim.ts +++ b/x-pack/legacy/plugins/canvas/server/shim.ts @@ -8,6 +8,7 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { Legacy } from 'kibana'; import { CoreSetup as ExistingCoreSetup } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginSetupContract } from '../../../../plugins/features/server'; export interface CoreSetup { @@ -32,7 +33,7 @@ export interface PluginsSetup { addSavedObjectsToSampleDataset: any; addAppLinksToSampleDataset: any; }; - usage: Legacy.Server['usage']; + usageCollection: UsageCollectionSetup; } export async function createSetupShim( @@ -68,7 +69,7 @@ export async function createSetupShim( // @ts-ignore: Missing from Legacy Server Type addAppLinksToSampleDataset: server.addAppLinksToSampleDataset, }, - usage: server.usage, + usageCollection: server.newPlatform.setup.plugins.usageCollection, }, }; } diff --git a/x-pack/legacy/plugins/canvas/server/usage/collector.ts b/x-pack/legacy/plugins/canvas/server/usage/collector.ts index 7e6ef31d93ba5..ae009f9265722 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/collector.ts @@ -5,7 +5,8 @@ */ import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { CoreSetup, PluginsSetup } from '../shim'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CoreSetup } from '../shim'; // @ts-ignore missing local declaration import { CANVAS_USAGE_TYPE } from '../../common/lib/constants'; import { workpadCollector } from './workpad_collector'; @@ -22,9 +23,12 @@ const collectors: TelemetryCollector[] = [workpadCollector, customElementCollect A usage collector function returns an object derived from current data in the ES Cluster. */ -export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsSetup) { - const kibanaIndex = setup.getServerConfig().get('kibana.index'); - const canvasCollector = plugins.usage.collectorSet.makeUsageCollector({ +export function registerCanvasUsageCollector( + usageCollection: UsageCollectionSetup, + core: CoreSetup +) { + const kibanaIndex = core.getServerConfig().get('kibana.index'); + const canvasCollector = usageCollection.makeUsageCollector({ type: CANVAS_USAGE_TYPE, isReady: () => true, fetch: async (callCluster: CallCluster) => { @@ -42,5 +46,5 @@ export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsS }, }); - plugins.usage.collectorSet.register(canvasCollector); + usageCollection.registerCollector(canvasCollector); } diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx index 9cf2ddc3a22e3..2ec3cfde8bd68 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx @@ -19,8 +19,6 @@ import { getScrubber as scrubber, getScrubberSlideContainer as scrubberContainer, getPageControlsCenter as center, - getSettingsTrigger as trigger, - getContextMenuItems as menuItems, // getAutoplayTextField as autoplayText, // getAutoplayCheckbox as autoplayCheck, // getAutoplaySubmit as autoplaySubmit, @@ -30,6 +28,7 @@ import { getPageControlsPrevious as previous, getPageControlsNext as next, } from '../../test/selectors'; +import { openSettings, selectMenuItem } from '../../test/interactions'; // Mock the renderers jest.mock('../../supported_renderers'); @@ -102,13 +101,9 @@ describe('', () => { test('autohide footer functions on mouseEnter + Leave', async () => { const wrapper = getWrapper(); - trigger(wrapper).simulate('click'); - await tick(20); - menuItems(wrapper) - .at(1) - .simulate('click'); - await tick(20); - wrapper.update(); + await openSettings(wrapper); + await selectMenuItem(wrapper, 1); + expect(footer(wrapper).prop('isHidden')).toEqual(false); expect(footer(wrapper).prop('isAutohide')).toEqual(false); toolbarCheck(wrapper).simulate('click'); @@ -125,13 +120,9 @@ describe('', () => { expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true); // Open the menu and activate toolbar hiding. - trigger(wrapper).simulate('click'); - await tick(20); - menuItems(wrapper) - .at(1) - .simulate('click'); - await tick(20); - wrapper.update(); + await openSettings(wrapper); + await selectMenuItem(wrapper, 1); + toolbarCheck(wrapper).simulate('click'); await tick(20); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot index 1e66e19b3c0e1..e9f496bfe6358 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot @@ -27,7 +27,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off, >
@@ -295,7 +313,7 @@ exports[` can navigate Autoplay Settings 2`] = ` >
@@ -566,6 +602,258 @@ exports[` can navigate Toolbar Settings, closes when activated 1`] =
`; -exports[` can navigate Toolbar Settings, closes when activated 2`] = `"
Settings

Hide Toolbar

Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 2`] = ` +
+
+
+
+
+ +
+
+
+`; -exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings

Hide Toolbar

Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings

Hide Toolbar

Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index 0667674b6a7dd..66515eb3421d5 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -7,7 +7,8 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { JestContext } from '../../../../test/context_jest'; -import { takeMountedSnapshot, tick } from '../../../../test'; +import { takeMountedSnapshot } from '../../../../test'; +import { openSettings, selectMenuItem } from '../../../../test/interactions'; import { getSettingsTrigger as trigger, getPopover as popover, @@ -60,36 +61,26 @@ describe('', () => { expect(popover(wrapper).prop('isOpen')).toEqual(false); }); - test.skip('can navigate Autoplay Settings', async () => { - trigger(wrapper).simulate('click'); + test('can navigate Autoplay Settings', async () => { + await openSettings(wrapper); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); - await tick(20); - menuItems(wrapper) - .at(0) - .simulate('click'); - await tick(20); + + await selectMenuItem(wrapper, 0); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); }); - test.skip('can navigate Toolbar Settings, closes when activated', async () => { - trigger(wrapper).simulate('click'); + test('can navigate Toolbar Settings, closes when activated', async () => { + await openSettings(wrapper); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); - menuItems(wrapper) - .at(1) - .simulate('click'); - // Wait for the animation and DOM update - await tick(40); - portal(wrapper).update(); - expect(portal(wrapper).html()).toMatchSnapshot(); + await selectMenuItem(wrapper, 1); + expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); // Click the Hide Toolbar switch portal(wrapper) .find('button[data-test-subj="hideToolbarSwitch"]') .simulate('click'); - // Wait for the animation and DOM update - await tick(20); portal(wrapper).update(); // The Portal should not be open. diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/test/interactions.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/test/interactions.ts new file mode 100644 index 0000000000000..1c5b78929aaa5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/test/interactions.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactWrapper } from 'enzyme'; +import { getSettingsTrigger, getPortal, getContextMenuItems } from './selectors'; +import { waitFor } from './utils'; + +export const openSettings = async function(wrapper: ReactWrapper) { + getSettingsTrigger(wrapper).simulate('click'); + + try { + // Wait for EuiPanel to be visible + await waitFor(() => { + wrapper.update(); + + return getPortal(wrapper) + .find('EuiPanel') + .exists(); + }); + } catch (e) { + throw new Error('Settings Panel did not open in given time'); + } +}; + +export const selectMenuItem = async function(wrapper: ReactWrapper, menuItemIndex: number) { + getContextMenuItems(wrapper) + .at(menuItemIndex) + .simulate('click'); + + try { + // When the menu item is clicked, wait for all of the context menus to be there + await waitFor(() => { + wrapper.update(); + return getPortal(wrapper).find('EuiContextMenuPanel').length === 2; + }); + } catch (e) { + throw new Error('Context menu did not transition'); + } +}; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/test/utils.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/test/utils.ts index 2e7bc4b262b52..4e18f2af1b06a 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/test/utils.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/test/utils.ts @@ -6,7 +6,6 @@ import { ReactWrapper } from 'enzyme'; import { Component } from 'react'; -import { setTimeout } from 'timers'; export const tick = (ms = 0) => new Promise(resolve => { @@ -19,3 +18,25 @@ export const takeMountedSnapshot = (mountedComponent: ReactWrapper<{}, {}, Compo template.innerHTML = html; return template.content.firstChild; }; + +export const waitFor = (fn: () => boolean, stepMs = 100, failAfterMs = 1000) => { + return new Promise((resolve, reject) => { + let waitForTimeout: NodeJS.Timeout; + + const tryCondition = () => { + if (fn()) { + clearTimeout(failTimeout); + resolve(); + } else { + waitForTimeout = setTimeout(tryCondition, stepMs); + } + }; + + const failTimeout = setTimeout(() => { + clearTimeout(waitForTimeout); + reject('wait for condition was never met'); + }, failAfterMs); + + tryCondition(); + }); +}; diff --git a/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts new file mode 100644 index 0000000000000..660cd256cebcd --- /dev/null +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 sinon from 'sinon'; +import { Server } from 'hapi'; +import { createCollectorFetch, createCloudUsageCollector } from './cloud_usage_collector'; + +const CLOUD_ID_STAGING = + 'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; +const CLOUD_ID = + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; + +const mockUsageCollection = () => ({ + makeUsageCollector: sinon.stub(), +}); + +const getMockServer = (cloudId?: string) => + ({ + config() { + return { + get(path: string) { + switch (path) { + case 'xpack.cloud': + return { id: cloudId }; + default: + throw Error(`server.config().get(${path}) should not be called by this collector.`); + } + }, + }; + }, + } as Server); + +describe('Cloud usage collector', () => { + describe('collector', () => { + it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => { + const mockServer = getMockServer(); + const collector = await createCollectorFetch(mockServer)(); + expect(collector.isCloudEnabled).toBe(false); + }); + + it('returns `isCloudEnabled: true` if `xpack.cloud.id` is defined', async () => { + const stagingCollector = await createCollectorFetch(getMockServer(CLOUD_ID))(); + const collector = await createCollectorFetch(getMockServer(CLOUD_ID_STAGING))(); + expect(collector.isCloudEnabled).toBe(true); + expect(stagingCollector.isCloudEnabled).toBe(true); + }); + }); +}); + +describe('createCloudUsageCollector', () => { + it('returns calls `makeUsageCollector`', () => { + const mockServer = getMockServer(); + const usageCollection = mockUsageCollection(); + createCloudUsageCollector(usageCollection as any, mockServer); + expect(usageCollection.makeUsageCollector.calledOnce).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts new file mode 100644 index 0000000000000..7fdf32144972c --- /dev/null +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { KIBANA_CLOUD_STATS_TYPE } from './constants'; + +export interface UsageStats { + isCloudEnabled: boolean; +} + +export function createCollectorFetch(server: Server) { + return async function fetchUsageStats(): Promise { + const { id } = server.config().get(`xpack.cloud`); + + return { + isCloudEnabled: !!id, + }; + }; +} + +export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + return usageCollection.makeUsageCollector({ + type: KIBANA_CLOUD_STATS_TYPE, + isReady: () => true, + fetch: createCollectorFetch(server), + }); +} + +export function registerCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + const collector = createCloudUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts b/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts deleted file mode 100644 index ee80875890480..0000000000000 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts +++ /dev/null @@ -1,58 +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 sinon from 'sinon'; -import { - createCollectorFetch, - getCloudUsageCollector, - KibanaHapiServer, -} from './get_cloud_usage_collector'; - -const CLOUD_ID_STAGING = - 'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; -const CLOUD_ID = - 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; - -const getMockServer = (cloudId?: string) => ({ - usage: { collectorSet: { makeUsageCollector: sinon.stub() } }, - config() { - return { - get(path: string) { - switch (path) { - case 'xpack.cloud': - return { id: cloudId }; - default: - throw Error(`server.config().get(${path}) should not be called by this collector.`); - } - }, - }; - }, -}); - -describe('Cloud usage collector', () => { - describe('collector', () => { - it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => { - const collector = await createCollectorFetch(getMockServer())(); - expect(collector.isCloudEnabled).toBe(false); - }); - - it('returns `isCloudEnabled: true` if `xpack.cloud.id` is defined', async () => { - const stagingCollector = await createCollectorFetch(getMockServer(CLOUD_ID))(); - const collector = await createCollectorFetch(getMockServer(CLOUD_ID_STAGING))(); - expect(collector.isCloudEnabled).toBe(true); - expect(stagingCollector.isCloudEnabled).toBe(true); - }); - }); -}); - -describe('getCloudUsageCollector', () => { - it('returns calls `collectorSet.makeUsageCollector`', () => { - const mockServer = getMockServer(); - getCloudUsageCollector((mockServer as any) as KibanaHapiServer); - const { makeUsageCollector } = mockServer.usage.collectorSet; - expect(makeUsageCollector.calledOnce).toBe(true); - }); -}); diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts b/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts deleted file mode 100644 index 5ce7be59a1c9c..0000000000000 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -import { KIBANA_CLOUD_STATS_TYPE } from './constants'; - -export interface UsageStats { - isCloudEnabled: boolean; -} - -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - }; - }; -} - -export function createCollectorFetch(server: any) { - return async function fetchUsageStats(): Promise { - const { id } = server.config().get(`xpack.cloud`); - - return { - isCloudEnabled: !!id, - }; - }; -} - -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function getCloudUsageCollector(server: KibanaHapiServer) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ - type: KIBANA_CLOUD_STATS_TYPE, - isReady: () => true, - fetch: createCollectorFetch(server), - }); -} diff --git a/x-pack/legacy/plugins/cloud/index.js b/x-pack/legacy/plugins/cloud/index.js index 0cca122b52316..c2fd35eea5292 100644 --- a/x-pack/legacy/plugins/cloud/index.js +++ b/x-pack/legacy/plugins/cloud/index.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCloudUsageCollector } from './get_cloud_usage_collector'; +import { registerCloudUsageCollector } from './cloud_usage_collector'; export const cloud = kibana => { return new kibana.Plugin({ @@ -40,7 +40,8 @@ export const cloud = kibana => { server.expose('config', { isCloudEnabled: !!config.id }); - server.usage.collectorSet.register(getCloudUsageCollector(server)); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCloudUsageCollector(usageCollection, server); } }); }; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 5f7ac218e1b98..8093c57d2631a 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -26,7 +26,6 @@ import 'uiExports/embeddableActions'; import 'uiExports/embeddableFactories'; import 'uiExports/navbarExtensions'; import 'uiExports/docViews'; -import 'uiExports/fieldFormats'; import 'uiExports/search'; import 'uiExports/shareContextMenuExtensions'; import _ from 'lodash'; @@ -38,6 +37,8 @@ import 'ui/agg_response'; import 'ui/agg_types'; import 'leaflet'; import { npStart } from 'ui/new_platform'; +import { localApplicationService } from 'plugins/kibana/local_application_service'; + import { showAppRedirectNotification } from 'ui/notify'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard/dashboard_constants'; @@ -45,6 +46,8 @@ import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashb uiModules.get('kibana') .config(dashboardConfigProvider => dashboardConfigProvider.turnHideWriteControlsOn()); +localApplicationService.attachToAngular(routes); + routes.enable(); routes.otherwise({ redirectTo: defaultUrl() }); diff --git a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts index 1c82c2b6237e1..0770899af5393 100644 --- a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts +++ b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts @@ -16,3 +16,5 @@ export const ES_GEO_FIELD_TYPE = { GEO_POINT: 'geo_point', GEO_SHAPE: 'geo_shape', }; + +export const DEFAULT_KBN_VERSION = 'kbnVersion'; diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index 24907082adb2c..1eefc0afa8f9c 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { fileUploadRoutes } from './server/routes/file_upload'; -import { makeUsageCollector } from './server/telemetry/'; -import mappings from './mappings'; +import { FileUploadPlugin } from './server/plugin'; +import { mappings } from './mappings'; export const fileUpload = kibana => { return new kibana.Plugin({ @@ -23,11 +21,24 @@ export const fileUpload = kibana => { }, init(server) { - const { xpack_main: xpackMainPlugin } = server.plugins; + const coreSetup = server.newPlatform.setup.core; + const { usageCollection } = server.newPlatform.setup.plugins; + const pluginsSetup = { + usageCollection, + }; - mirrorPluginStatus(xpackMainPlugin, this); - fileUploadRoutes(server); - makeUsageCollector(server); + // legacy dependencies + const __LEGACY = { + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + }, + savedObjects: { + getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository + }, + }; + + new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); } }); }; diff --git a/x-pack/legacy/plugins/file_upload/mappings.json b/x-pack/legacy/plugins/file_upload/mappings.json deleted file mode 100644 index addff6308d3f0..0000000000000 --- a/x-pack/legacy/plugins/file_upload/mappings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - } -} diff --git a/x-pack/legacy/plugins/file_upload/mappings.ts b/x-pack/legacy/plugins/file_upload/mappings.ts new file mode 100644 index 0000000000000..70229c7088324 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/mappings.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mappings = { + 'file-upload-telemetry': { + properties: { + filesUploadedTotalCount: { + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js index 9e553a536845d..9c6248049d9cf 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js @@ -8,7 +8,7 @@ import React, { Fragment, Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTitle, EuiProgress, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; +import { basePath } from '../kibana_services'; export class JsonImportProgress extends Component { @@ -114,7 +114,7 @@ export class JsonImportProgress extends Component { diff --git a/x-pack/legacy/plugins/file_upload/public/index.js b/x-pack/legacy/plugins/file_upload/public/index.js deleted file mode 100644 index a02b82170f70f..0000000000000 --- a/x-pack/legacy/plugins/file_upload/public/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { JsonUploadAndParse } from './components/json_upload_and_parse'; diff --git a/x-pack/legacy/plugins/file_upload/public/index.ts b/x-pack/legacy/plugins/file_upload/public/index.ts new file mode 100644 index 0000000000000..205ceae37d6a1 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FileUploadPlugin } from './plugin'; + +export function plugin() { + return new FileUploadPlugin(); +} diff --git a/x-pack/legacy/plugins/file_upload/public/kibana_services.js b/x-pack/legacy/plugins/file_upload/public/kibana_services.js index 10a6ae7179bc2..3c00ab5709660 100644 --- a/x-pack/legacy/plugins/file_upload/public/kibana_services.js +++ b/x-pack/legacy/plugins/file_upload/public/kibana_services.js @@ -5,5 +5,18 @@ */ import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { DEFAULT_KBN_VERSION } from '../common/constants/file_import'; export const indexPatternService = data.indexPatterns.indexPatterns; + +export let savedObjectsClient; +export let basePath; +export let apiBasePath; +export let kbnVersion; + +export const initServicesAndConstants = ({ savedObjects, http, injectedMetadata }) => { + savedObjectsClient = savedObjects.client; + basePath = http.basePath.basePath; + apiBasePath = http.basePath.prepend('/api'); + kbnVersion = injectedMetadata.getKibanaVersion(DEFAULT_KBN_VERSION); +}; diff --git a/x-pack/legacy/plugins/file_upload/public/legacy.ts b/x-pack/legacy/plugins/file_upload/public/legacy.ts new file mode 100644 index 0000000000000..719599df3ccbe --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/legacy.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npStart } from 'ui/new_platform'; +import { plugin } from '.'; + +const pluginInstance = plugin(); + +export const start = pluginInstance.start(npStart.core); diff --git a/x-pack/legacy/plugins/file_upload/public/plugin.ts b/x-pack/legacy/plugins/file_upload/public/plugin.ts new file mode 100644 index 0000000000000..cc9ebbfc15b39 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreStart } from 'src/core/public'; +// @ts-ignore +import { initResources } from './util/indexing_service'; +// @ts-ignore +import { JsonUploadAndParse } from './components/json_upload_and_parse'; +// @ts-ignore +import { initServicesAndConstants } from './kibana_services'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type FileUploadPluginSetup = ReturnType; +export type FileUploadPluginStart = ReturnType; + +/** @internal */ +export class FileUploadPlugin implements Plugin { + public setup() {} + + public start(core: CoreStart) { + initServicesAndConstants(core); + return { + JsonUploadAndParse, + }; + } +} diff --git a/x-pack/legacy/plugins/file_upload/public/util/http_service.js b/x-pack/legacy/plugins/file_upload/public/util/http_service.js index 26d46cecb0e51..a744f0f075490 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/http_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/http_service.js @@ -6,9 +6,9 @@ // service for interacting with the server -import chrome from 'ui/chrome'; import { addSystemApiHeader } from 'ui/system_api'; import { i18n } from '@kbn/i18n'; +import { kbnVersion } from '../kibana_services'; export async function http(options) { if(!(options && options.url)) { @@ -20,7 +20,7 @@ export async function http(options) { const url = options.url || ''; const headers = addSystemApiHeader({ 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), + 'kbn-version': kbnVersion, ...options.headers }); diff --git a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js index fd96bd95d4bb7..b40659ec4b513 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { http } from './http_service'; -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { indexPatternService } from '../kibana_services'; +import { http as httpService } from './http_service'; +import { + indexPatternService, + apiBasePath, + savedObjectsClient +} from '../kibana_services'; import { getGeoJsonIndexingDetails } from './geo_processing'; import { sizeLimitedChunking } from './size_limited_chunking'; +import { i18n } from '@kbn/i18n'; -const basePath = chrome.addBasePath('/api/fileupload'); const fileType = 'json'; export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { @@ -19,7 +21,6 @@ export async function indexData(parsedFile, transformDetails, indexName, dataTyp throw(i18n.translate('xpack.fileUpload.indexingService.noFileImported', { defaultMessage: 'No file imported.' })); - return; } // Perform any processing required on file prior to indexing @@ -129,8 +130,8 @@ async function writeToIndex(indexingDetails) { ingestPipeline } = indexingDetails; - return await http({ - url: `${basePath}/import${paramString}`, + return await httpService({ + url: `${apiBasePath}/fileupload/import${paramString}`, method: 'POST', data: { index, @@ -223,7 +224,6 @@ export async function createIndexPattern(indexPatternName) { } async function getIndexPatternId(name) { - const savedObjectsClient = chrome.getSavedObjectsClient(); const savedObjectSearch = await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); const indexPatternSavedObjects = savedObjectSearch.savedObjects; @@ -237,9 +237,8 @@ async function getIndexPatternId(name) { } export const getExistingIndexNames = async () => { - const basePath = chrome.addBasePath('/api'); - const indexes = await http({ - url: `${basePath}/index_management/indices`, + const indexes = await httpService({ + url: `${apiBasePath}/index_management/indices`, method: 'GET', }); return indexes @@ -248,7 +247,6 @@ export const getExistingIndexNames = async () => { }; export const getExistingIndexPatternNames = async () => { - const savedObjectsClient = chrome.getSavedObjectsClient(); const indexPatterns = await savedObjectsClient.find({ type: 'index-pattern', fields: ['id', 'title', 'type', 'fields'], diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts index 0b39c81cee6ff..9c1000db8cb56 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; - -export function callWithInternalUserFactory(server: Server): any; +export function callWithInternalUserFactory(elasticsearchPlugin: any): any; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js index dc3131484e75f..f42c3ffb99a5b 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js @@ -5,16 +5,15 @@ */ - import { once } from 'lodash'; -const _callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); +const _callWithInternalUser = once(elasticsearchPlugin => { + const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); return callWithInternalUser; }); -export const callWithInternalUserFactory = (server) => { +export const callWithInternalUserFactory = elasticsearchPlugin => { return (...args) => { - return _callWithInternalUser(server)(...args); + return _callWithInternalUser(elasticsearchPlugin)(...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts index d77541e7d3d6c..04c5013ed8e67 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts @@ -8,25 +8,15 @@ import { callWithInternalUserFactory } from './call_with_internal_user_factory'; describe('call_with_internal_user_factory', () => { describe('callWithInternalUserFactory', () => { - let server: any; - let callWithInternalUser: any; - - beforeEach(() => { - callWithInternalUser = jest.fn(); - server = { - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, - }; - }); - it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(server); + const callWithInternalUser: any = jest.fn(); + const elasticsearchPlugin: any = { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }; + const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); callWithInternalUserInstance(); - expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('admin'); + expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); }); }); }); diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js index 0040fcb6c802a..885573c993b7f 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js @@ -8,13 +8,13 @@ import { once } from 'lodash'; -const callWithRequest = once((server) => { - const cluster = server.plugins.elasticsearch.getCluster('data'); +const callWithRequest = once(elasticsearchPlugin => { + const cluster = elasticsearchPlugin.getCluster('data'); return cluster.callWithRequest; }); -export const callWithRequestFactory = (server, request) => { +export const callWithRequestFactory = (elasticsearchPlugin, request) => { return (...args) => { - return callWithRequest(server)(request, ...args); + return callWithRequest(elasticsearchPlugin)(request, ...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js new file mode 100644 index 0000000000000..d9819bf26faea --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getImportRouteHandler } from './routes/file_upload'; +import { MAX_BYTES } from '../common/constants/file_import'; +import { registerFileUploadUsageCollector } from './telemetry'; + +export class FileUploadPlugin { + setup(core, plugins, __LEGACY) { + const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; + const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; + + // Set up route + __LEGACY.route({ + method: 'POST', + path: '/api/fileupload/import', + handler: getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository), + config: { + payload: { maxBytes: MAX_BYTES }, + } + }); + + registerFileUploadUsageCollector(plugins.usageCollection, { + elasticsearchPlugin, + getSavedObjectsRepository, + }); + } +} diff --git a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js index ac07d80962bdc..1eeecdeb1525b 100644 --- a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js @@ -7,7 +7,6 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; import { importDataProvider } from '../models/import_data'; -import { MAX_BYTES } from '../../common/constants/file_import'; import { updateTelemetry } from '../telemetry/telemetry'; @@ -18,28 +17,35 @@ function importData({ return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } -export function fileUploadRoutes(server, commonRouteConfig) { +export function getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository) { + return async request => { - server.route({ - method: 'POST', - path: '/api/fileupload/import', - async handler(request) { + const requestObj = { + query: request.query, + payload: request.payload, + params: request.payload, + auth: request.auth, + headers: request.headers + }; - // `id` being `undefined` tells us that this is a new import due to create a new index. - // follow-up import calls to just add additional data will include the `id` of the created - // index, we'll ignore those and don't increment the counter. - const { id } = request.query; - if (id === undefined) { - await updateTelemetry({ server, ...request.payload }); - } - - const callWithRequest = callWithRequestFactory(server, request); - return importData({ callWithRequest, id, ...request.payload }) - .catch(wrapError); - }, - config: { - ...commonRouteConfig, - payload: { maxBytes: MAX_BYTES }, + // `id` being `undefined` tells us that this is a new import due to create a new index. + // follow-up import calls to just add additional data will include the `id` of the created + // index, we'll ignore those and don't increment the counter. + const { id } = requestObj.query; + if (id === undefined) { + await updateTelemetry({ elasticsearchPlugin, getSavedObjectsRepository }); } - }); + + const requestContentWithDefaults = { + id, + callWithRequest: callWithRequestFactory(elasticsearchPlugin, requestObj), + index: undefined, + settings: {}, + mappings: {}, + ingestPipeline: {}, + data: [], + ...requestObj.payload + }; + return importData(requestContentWithDefaults).catch(wrapError); + }; } diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts new file mode 100644 index 0000000000000..a2b359ae11638 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getTelemetry, initTelemetry } from './telemetry'; + +const TELEMETRY_TYPE = 'fileUploadTelemetry'; + +export function registerFileUploadUsageCollector( + usageCollection: UsageCollectionSetup, + deps: { + elasticsearchPlugin: any; + getSavedObjectsRepository: any; + } +): void { + const { elasticsearchPlugin, getSavedObjectsRepository } = deps; + const fileUploadUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => + (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(), + }); + + usageCollection.registerCollector(fileUploadUsageCollector); +} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts index d05f7cc63c896..7969dd04ce31f 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './telemetry'; -export { makeUsageCollector } from './make_usage_collector'; +export { registerFileUploadUsageCollector } from './file_upload_usage_collector'; diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts deleted file mode 100644 index f589280d8cf3a..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.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 { Server } from 'hapi'; -import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; - -// TODO this type should be defined by the platform -interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - register: any; - }; - }; -} - -export function makeUsageCollector(server: KibanaHapiServer): void { - const fileUploadUsageCollector = server.usage.collectorSet.makeUsageCollector({ - type: 'fileUploadTelemetry', - isReady: () => true, - fetch: async (): Promise => (await getTelemetry(server)) || initTelemetry(), - }); - server.usage.collectorSet.register(fileUploadUsageCollector); -} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts index 5017c9cb41f08..1c785d8e7b61c 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -6,22 +6,13 @@ import { getTelemetry, updateTelemetry } from './telemetry'; +const elasticsearchPlugin: any = null; +const getSavedObjectsRepository: any = null; const internalRepository = () => ({ get: jest.fn(() => null), create: jest.fn(() => ({ attributes: 'test' })), update: jest.fn(() => ({ attributes: 'test' })), }); -const server: any = { - savedObjects: { - getSavedObjectsRepository: jest.fn(() => internalRepository()), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, -}; -const callWithInternalUser = jest.fn(); function mockInit(getVal: any = { attributes: {} }): any { return { @@ -34,7 +25,7 @@ describe('file upload plugin telemetry', () => { describe('getTelemetry', () => { it('should get existing telemetry', async () => { const internalRepo = mockInit(); - await getTelemetry(server, internalRepo); + await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository, internalRepo); expect(internalRepo.update.mock.calls.length).toBe(0); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); @@ -48,7 +39,12 @@ describe('file upload plugin telemetry', () => { filesUploadedTotalCount: 2, }, }); - await updateTelemetry({ server, internalRepo }); + + await updateTelemetry({ + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepo, + }); expect(internalRepo.update.mock.calls.length).toBe(1); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts index b43e2a1b33a29..5ffa735f4c83a 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import _ from 'lodash'; import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; @@ -18,9 +17,11 @@ export interface TelemetrySavedObject { attributes: Telemetry; } -export function getInternalRepository(server: Server): any { - const { getSavedObjectsRepository } = server.savedObjects; - const callWithInternalUser = callWithInternalUserFactory(server); +export function getInternalRepository( + elasticsearchPlugin: any, + getSavedObjectsRepository: any +): any { + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); return getSavedObjectsRepository(callWithInternalUser); } @@ -30,8 +31,13 @@ export function initTelemetry(): Telemetry { }; } -export async function getTelemetry(server: Server, internalRepo?: object): Promise { - const internalRepository = internalRepo || getInternalRepository(server); +export async function getTelemetry( + elasticsearchPlugin: any, + getSavedObjectsRepository: any, + internalRepo?: object +): Promise { + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); let telemetrySavedObject; try { @@ -44,14 +50,21 @@ export async function getTelemetry(server: Server, internalRepo?: object): Promi } export async function updateTelemetry({ - server, + elasticsearchPlugin, + getSavedObjectsRepository, internalRepo, }: { - server: any; + elasticsearchPlugin: any; + getSavedObjectsRepository: any; internalRepo?: any; }) { - const internalRepository = internalRepo || getInternalRepository(server); - let telemetry = await getTelemetry(server, internalRepository); + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); + let telemetry = await getTelemetry( + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepository + ); // Create if doesn't exist if (!telemetry || _.isEmpty(telemetry)) { const newTelemetrySavedObject = await internalRepository.create( diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index 8dede207b803c..5fae9720db39a 100644 --- a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { connect } from 'react-redux'; -import { IDataPluginServices } from 'src/legacy/core_plugins/data/public/types'; +import { IDataPluginServices } from 'src/plugins/data/public'; import { GraphState, hasDatasourceSelector, diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index b6200d831b248..56458e5de273f 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -9,13 +9,9 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { IDataPluginServices } from 'src/legacy/core_plugins/data/public/types'; -import { Query } from 'src/plugins/data/public'; import { IndexPatternSavedObject, IndexPatternProvider } from '../types'; import { QueryBarInput, IndexPattern } from '../../../../../../src/legacy/core_plugins/data/public'; import { openSourceModal } from '../services/source_modal'; - import { GraphState, datasourceSelector, @@ -24,6 +20,7 @@ import { } from '../state_management'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { IDataPluginServices, Query, esKuery } from '../../../../../../src/plugins/data/public'; export interface OuterSearchBarProps { isLoading: boolean; @@ -45,7 +42,10 @@ export interface SearchBarProps extends OuterSearchBarProps { function queryToString(query: Query, indexPattern: IndexPattern) { if (query.language === 'kuery' && typeof query.query === 'string') { - const dsl = toElasticsearchQuery(fromKueryExpression(query.query as string), indexPattern); + const dsl = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); // JSON representation of query will be handled by existing logic. // TODO clean this up and handle it in the data fetch layer once // it moved to typescript. diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 48420d403653f..988aa78695095 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -6,7 +6,6 @@ // legacy imports currently necessary to power Graph // for a cutover all of these have to be resolved -import 'uiExports/fieldFormats'; import 'uiExports/savedObjectTypes'; import 'uiExports/autocompleteProviders'; import 'ui/autoload/all'; @@ -20,6 +19,7 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis import { npSetup, npStart } from 'ui/new_platform'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy'; import { GraphPlugin } from './plugin'; // @ts-ignore @@ -53,6 +53,7 @@ async function getAngularInjectedDependencies(): Promise; + navigation: NavigationStart; } export interface GraphPluginSetupDependencies { @@ -30,6 +32,7 @@ export interface GraphPluginStartDependencies { export class GraphPlugin implements Plugin { private dataStart: DataStart | null = null; + private navigationStart: NavigationStart | null = null; private npDataStart: ReturnType | null = null; private savedObjectsClient: SavedObjectsClientContract | null = null; private angularDependencies: LegacyAngularInjectedDependencies | null = null; @@ -42,6 +45,7 @@ export class GraphPlugin implements Plugin { const { renderApp } = await import('./render_app'); return renderApp({ ...params, + navigation: this.navigationStart!, npData: this.npDataStart!, savedObjectsClient: this.savedObjectsClient!, xpackInfo, @@ -66,9 +70,9 @@ export class GraphPlugin implements Plugin { start( core: CoreStart, - { data, npData, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies + { data, npData, navigation, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies ) { - // TODO is this really the right way? I though the app context would give us those + this.navigationStart = navigation; this.dataStart = data; this.npDataStart = npData; this.angularDependencies = angularDependencies; diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index a8a86f4d1f850..18cdf0ddd81b2 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -25,6 +25,7 @@ import { DataStart } from 'src/legacy/core_plugins/data/public'; import { AppMountContext, ChromeStart, + LegacyCoreStart, SavedObjectsClientContract, ToastsStart, UiSettingsClientContract, @@ -32,6 +33,7 @@ import { // @ts-ignore import { initGraphApp } from './app'; import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; +import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; /** * These are dependencies of the Graph app besides the base dependencies @@ -44,6 +46,7 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { appBasePath: string; capabilities: Record>; coreStart: AppMountContext['core']; + navigation: NavigationStart; chrome: ChromeStart; config: UiSettingsClientContract; toastNotifications: ToastsStart; @@ -75,8 +78,8 @@ export interface LegacyAngularInjectedDependencies { } export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { - const graphAngularModule = createLocalAngularModule(deps.coreStart); - configureAppAngularModule(graphAngularModule); + const graphAngularModule = createLocalAngularModule(deps.navigation); + configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true); initGraphApp(graphAngularModule, deps); const $injector = mountGraphApp(appBasePath, element); return () => $injector.get('$rootScope').$destroy(); @@ -104,9 +107,9 @@ function mountGraphApp(appBasePath: string, element: HTMLElement) { return $injector; } -function createLocalAngularModule(core: AppMountContext['core']) { +function createLocalAngularModule(navigation: NavigationStart) { createLocalI18nModule(); - createLocalTopNavModule(); + createLocalTopNavModule(navigation); createLocalConfirmModalModule(); const graphAngularModule = angular.module(moduleName, [ @@ -125,11 +128,11 @@ function createLocalConfirmModalModule() { .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); } -function createLocalTopNavModule() { +function createLocalTopNavModule(navigation: NavigationStart) { angular .module('graphTopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper); + .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); } function createLocalI18nModule() { diff --git a/x-pack/legacy/plugins/grokdebugger/public/register.js b/x-pack/legacy/plugins/grokdebugger/public/register.js index 74679d65e52d2..8201fed5b2220 100644 --- a/x-pack/legacy/plugins/grokdebugger/public/register.js +++ b/x-pack/legacy/plugins/grokdebugger/public/register.js @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { npSetup, npStart } from 'ui/new_platform'; -npSetup.plugins.devTools.register({ +npSetup.plugins.dev_tools.register({ order: 6, title: i18n.translate('xpack.grokDebugger.displayName', { defaultMessage: 'Grok Debugger', diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js index a87999873e40f..616aefaf73f62 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { GrokdebuggerRequest } from '../grokdebugger_request'; -describe('grokdebugger_request', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('grokdebugger_request', () => { describe('GrokdebuggerRequest', () => { const downstreamRequest = { diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts index 38684cb22e237..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts new file mode 100644 index 0000000000000..727faca69298e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts new file mode 100644 index 0000000000000..62d81dc136853 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts @@ -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 * as rt from 'io-ts'; + +export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; + +/** + * Request types + */ +export const validationIndicesRequestPayloadRT = rt.type({ + data: rt.type({ + timestampField: rt.string, + indices: rt.array(rt.string), + }), +}); + +export type ValidationIndicesRequestPayload = rt.TypeOf; + +/** + * Response types + * */ +export const validationIndicesErrorRT = rt.union([ + rt.type({ + error: rt.literal('INDEX_NOT_FOUND'), + index: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_FOUND'), + index: rt.string, + field: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_VALID'), + index: rt.string, + field: rt.string, + }), +]); + +export type ValidationIndicesError = rt.TypeOf; + +export const validationIndicesResponsePayloadRT = rt.type({ + data: rt.type({ + errors: rt.array(validationIndicesErrorRT), + }), +}); + +export type ValidationIndicesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/docs/arch_client.md b/x-pack/legacy/plugins/infra/docs/arch_client.md index 2be9de469f0ee..cdc4746357216 100644 --- a/x-pack/legacy/plugins/infra/docs/arch_client.md +++ b/x-pack/legacy/plugins/infra/docs/arch_client.md @@ -14,10 +14,10 @@ The `apps` folder contains the entry point for the UI code, such as for use in K - All components, please use Styled-Components. This also applies to small tweaks to EUI, just use `styled(Component)` and the `attrs` method for always used props. For example: ```jsx -export const Toolbar = styled(EuiPanel).attrs({ +export const Toolbar = styled(EuiPanel).attrs(() => ({ paddingSize: 'none', grow: false, -})` +}))` margin: -2px; `; ``` diff --git a/x-pack/legacy/plugins/infra/package.json b/x-pack/legacy/plugins/infra/package.json index 63812bb2da513..7aa8cb9b5269a 100644 --- a/x-pack/legacy/plugins/infra/package.json +++ b/x-pack/legacy/plugins/infra/package.json @@ -16,4 +16,4 @@ "boom": "7.3.0", "lodash": "^4.17.15" } -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 255e5b75c1390..4c215835ca240 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -101,7 +101,7 @@ export class AutocompleteField extends React.Component< } } - public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + public componentDidUpdate(prevProps: AutocompleteFieldProps) { const hasNewValue = prevProps.value !== this.props.value; const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; @@ -302,7 +302,7 @@ const withUnfocused = (state: AutocompleteFieldState) => ({ isFocused: false, }); -const FixedEuiFieldSearch: React.SFC & +const FixedEuiFieldSearch: React.FC & EuiFieldSearchProps & { inputRef?: (element: HTMLInputElement | null) => void; onSearch: (value: string) => void; @@ -312,10 +312,10 @@ const AutocompleteContainer = euiStyled.div` position: relative; `; -const SuggestionsPanel = euiStyled(EuiPanel).attrs({ +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ paddingSize: 'none', hasShadow: true, -})` +}))` position: absolute; width: 100%; margin-top: 2px; diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx index d7c9876a07a8d..0c29b1f51b07e 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -18,7 +18,7 @@ interface Props { suggestion: AutocompleteSuggestion; } -export const SuggestionItem: React.SFC = props => { +export const SuggestionItem: React.FC = props => { const { isSelected, onClick, onMouseEnter, suggestion } = props; return ( @@ -57,7 +57,7 @@ const SuggestionItemField = euiStyled.div` padding: ${props => props.theme.eui.euiSizeXS}; `; -const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>` +const SuggestionItemIconField = euiStyled(SuggestionItemField)<{ suggestionType: string }>` background-color: ${props => transparentize(0.9, getEuiIconColor(props.theme, props.suggestionType))}; color: ${props => getEuiIconColor(props.theme, props.suggestionType)}; @@ -66,12 +66,12 @@ const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: str width: ${props => props.theme.eui.euiSizeXL}; `; -const SuggestionItemTextField = SuggestionItemField.extend` +const SuggestionItemTextField = euiStyled(SuggestionItemField)` flex: 2 0 0; font-family: ${props => props.theme.eui.euiCodeFontFamily}; `; -const SuggestionItemDescriptionField = SuggestionItemField.extend` +const SuggestionItemDescriptionField = euiStyled(SuggestionItemField)` flex: 3 0 0; p { diff --git a/x-pack/legacy/plugins/infra/public/components/empty_states/no_data.tsx b/x-pack/legacy/plugins/infra/public/components/empty_states/no_data.tsx index be0f1eb78efee..7519e3fe10779 100644 --- a/x-pack/legacy/plugins/infra/public/components/empty_states/no_data.tsx +++ b/x-pack/legacy/plugins/infra/public/components/empty_states/no_data.tsx @@ -17,7 +17,7 @@ interface NoDataProps { testString?: string; } -export const NoData: React.SFC = ({ +export const NoData: React.FC = ({ titleText, bodyText, refetchText, diff --git a/x-pack/legacy/plugins/infra/public/components/empty_states/no_indices.tsx b/x-pack/legacy/plugins/infra/public/components/empty_states/no_indices.tsx index a1aadd7542c14..bfe282d2cee04 100644 --- a/x-pack/legacy/plugins/infra/public/components/empty_states/no_indices.tsx +++ b/x-pack/legacy/plugins/infra/public/components/empty_states/no_indices.tsx @@ -16,7 +16,7 @@ interface NoIndicesProps { 'data-test-subj'?: string; } -export const NoIndices: React.SFC = ({ actions, message, title, ...rest }) => ( +export const NoIndices: React.FC = ({ actions, message, title, ...rest }) => ( {title}} body={

{message}

} diff --git a/x-pack/legacy/plugins/infra/public/components/error_page.tsx b/x-pack/legacy/plugins/infra/public/components/error_page.tsx index cf16229a8dc4b..fea76c83292c2 100644 --- a/x-pack/legacy/plugins/infra/public/components/error_page.tsx +++ b/x-pack/legacy/plugins/infra/public/components/error_page.tsx @@ -24,7 +24,7 @@ interface Props { shortMessage: React.ReactNode; } -export const ErrorPage: React.SFC = ({ detailedMessage, retry, shortMessage }) => ( +export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessage }) => ( ({ grow: false, paddingSize: 'none', -})` +}))` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx index 0560c42d7498b..3095230ab8311 100644 --- a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx +++ b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx @@ -4,37 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink } from '@elastic/eui'; import React, { useEffect } from 'react'; -import ReactDOM from 'react-dom'; import chrome from 'ui/chrome'; interface HelpCenterContentProps { feedbackLink: string; - feedbackLinkText: string; + appName: string; } -const Content: React.FC = ({ feedbackLink, feedbackLinkText }) => ( - - {feedbackLinkText} - -); - -export const HelpCenterContent: React.FC = ({ - feedbackLink, - feedbackLinkText, -}) => { +export const HelpCenterContent: React.FC = ({ feedbackLink, appName }) => { useEffect(() => { - chrome.helpExtension.set(domElement => { - ReactDOM.render( - , - domElement - ); - return () => { - ReactDOM.unmountComponentAtNode(domElement); - }; + chrome.helpExtension.set({ + appName, + links: [ + { + linkType: 'discuss', + href: feedbackLink, + }, + ], }); - }, [feedbackLink, feedbackLinkText]); + }, [feedbackLink, appName]); return null; }; diff --git a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx index a99b265fc3ea9..5df1fc07e83b9 100644 --- a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx @@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div` position: absolute; top: 0; width: 100%; + z-index: ${props => props.theme.eui.euiZLevel1}; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx index 6fcb1779cba0c..24a5e8bacb4f9 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx @@ -149,11 +149,11 @@ const goToNextHighlightLabel = i18n.translate( } ); -const ActiveHighlightsIndicator = euiStyled(EuiIcon).attrs({ +const ActiveHighlightsIndicator = euiStyled(EuiIcon).attrs(({ theme }) => ({ type: 'checkInCircleFilled', size: 'm', - color: props => props.theme.eui.euiColorAccent, -})` + color: theme.eui.euiColorAccent, +}))` padding-left: ${props => props.theme.eui.paddingSizes.xs}; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx index d928ac678918b..b31afe6abea28 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/density_chart.tsx @@ -20,7 +20,7 @@ interface DensityChartProps { height: number; } -export const DensityChart: React.SFC = ({ +export const DensityChart: React.FC = ({ buckets, start, end, diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx index 08902f2b1644b..4711a7ac6ffde 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx @@ -17,7 +17,7 @@ interface HighlightedIntervalProps { target: number | null; } -export const HighlightedInterval: React.SFC = ({ +export const HighlightedInterval: React.FC = ({ className, end, getPositionOfTime, diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx index 90b1e455d477e..ad47d17fe3db9 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx @@ -238,7 +238,7 @@ export class LogMinimap extends React.Component { + ref={node => { this.dragTargetArea = node; }} x={0} diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx index eb49c8d010a90..c72403539563d 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx @@ -17,7 +17,7 @@ interface TimeRulerProps { width: number; } -export const TimeRuler: React.SFC = ({ end, height, start, tickCount, width }) => { +export const TimeRuler: React.FC = ({ end, height, start, tickCount, width }) => { const yScale = scaleTime() .domain([start, end]) .range([0, height]); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_statusbar.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_statusbar.tsx index c52ef4a5062dc..4bda5a5a4b009 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_statusbar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_statusbar.tsx @@ -8,11 +8,11 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import euiStyled from '../../../../../common/eui_styled_components'; -export const LogStatusbar = euiStyled(EuiFlexGroup).attrs({ +export const LogStatusbar = euiStyled(EuiFlexGroup).attrs(() => ({ alignItems: 'center', gutterSize: 'none', justifyContent: 'flexEnd', -})` +}))` padding: ${props => props.theme.eui.euiSizeS}; border-top: ${props => props.theme.eui.euiBorderThin}; max-height: 48px; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index 56a84d258c907..bf4a09769d254 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -79,9 +79,9 @@ const LogColumnHeader: React.FunctionComponent<{ ); -const LogColumnHeadersWrapper = euiStyled.div.attrs({ +const LogColumnHeadersWrapper = euiStyled.div.attrs(() => ({ role: 'row', -})` +}))` align-items: stretch; display: flex; flex-direction: row; @@ -95,9 +95,9 @@ const LogColumnHeadersWrapper = euiStyled.div.attrs({ z-index: 1; `; -const LogColumnHeaderWrapper = LogEntryColumn.extend.attrs({ +const LogColumnHeaderWrapper = euiStyled(LogEntryColumn).attrs(() => ({ role: 'columnheader', -})` +}))` align-items: center; display: flex; flex-direction: row; @@ -105,7 +105,7 @@ const LogColumnHeaderWrapper = LogEntryColumn.extend.attrs({ overflow: hidden; `; -const LogColumnHeaderContent = LogEntryColumnContent.extend` +const LogColumnHeaderContent = euiStyled(LogEntryColumnContent)` color: ${props => props.theme.eui.euiTitleColor}; font-size: ${props => props.theme.eui.euiFontSizeS}; font-weight: ${props => props.theme.eui.euiFontWeightSemiBold}; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx index 752a0d6e27a7f..549ca4c1ae047 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx @@ -114,7 +114,7 @@ interface ProgressEntryProps { isLoading: boolean; } -const ProgressEntry: React.SFC = props => { +const ProgressEntry: React.FC = props => { const { alignment, children, className, color, isLoading } = props; // NOTE: styled-components seems to make all props in EuiProgress required, so this diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx index 3df727789da74..643f98018cb0a 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx @@ -22,9 +22,9 @@ interface LogEntryColumnProps { shrinkWeight: number; } -export const LogEntryColumn = euiStyled.div.attrs({ +export const LogEntryColumn = euiStyled.div.attrs(() => ({ role: 'cell', -})` +}))` align-items: stretch; display: flex; flex-basis: ${props => props.baseWidth || '0%'}; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index cf2f67f3eb126..6252b3a396d1b 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -8,7 +8,7 @@ import stringify from 'json-stable-stringify'; import { darken, transparentize } from 'polished'; import React, { useMemo } from 'react'; -import styled, { css } from '../../../../../../common/eui_styled_components'; +import euiStyled, { css } from '../../../../../../common/eui_styled_components'; import { isFieldColumn, isHighlightFieldColumn, @@ -83,7 +83,7 @@ const unwrappedContentStyle = css` white-space: pre; `; -const CommaSeparatedLi = styled.li` +const CommaSeparatedLi = euiStyled.li` display: inline; &:not(:last-child) { margin-right: 1ex; @@ -93,11 +93,13 @@ const CommaSeparatedLi = styled.li` } `; -const FieldColumnContent = LogEntryColumnContent.extend.attrs<{ +interface LogEntryColumnContentProps { isHighlighted: boolean; isHovered: boolean; isWrapped?: boolean; -}>({})` +} + +const FieldColumnContent = euiStyled(LogEntryColumnContent)` background-color: ${props => props.theme.eui.euiColorEmptyShade}; text-overflow: ellipsis; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx index 8e55caae738e7..f7d841bcce94f 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx @@ -47,10 +47,12 @@ export const LogEntryDetailsIconColumn: React.FunctionComponent({})` +} + +const IconColumnContent = euiStyled(LogEntryColumnContent)` background-color: ${props => props.theme.eui.euiColorEmptyShade}; overflow: hidden; user-select: none; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx index 3e6b1dc48e89d..11d73736463e2 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx @@ -6,7 +6,7 @@ import React, { memo, useMemo } from 'react'; -import { css } from '../../../../../../common/eui_styled_components'; +import euiStyled, { css } from '../../../../../../common/eui_styled_components'; import { isConstantSegment, isFieldSegment, @@ -62,11 +62,13 @@ const unwrappedContentStyle = css` white-space: pre; `; -const MessageColumnContent = LogEntryColumnContent.extend.attrs<{ +interface MessageColumnContentProps { isHovered: boolean; isHighlighted: boolean; isWrapped?: boolean; -}>({})` +} + +const MessageColumnContent = euiStyled(LogEntryColumnContent)` background-color: ${props => props.theme.eui.euiColorEmptyShade}; text-overflow: ellipsis; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index ce3c0cf0f5a22..0da601ae52088 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -103,7 +103,7 @@ export const LogEntryRow = ({ return ( ({ +} + +const LogEntryRowWrapper = euiStyled.div.attrs(() => ({ role: 'row', -})` +}))` align-items: stretch; color: ${props => props.theme.eui.euiTextColor}; display: flex; @@ -204,5 +206,5 @@ const LogEntryRowWrapper = euiStyled.div.attrs<{ justify-content: flex-start; overflow: hidden; - ${props => monospaceTextStyle(props.scale)} + ${props => monospaceTextStyle(props.scale)}; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx index 884e5ff0a5bde..8e161367b428d 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx @@ -7,7 +7,7 @@ import { darken, transparentize } from 'polished'; import React, { memo } from 'react'; -import { css } from '../../../../../../common/eui_styled_components'; +import euiStyled, { css } from '../../../../../../common/eui_styled_components'; import { useFormattedTime } from '../../formatted_time'; import { LogEntryColumnContent } from './log_entry_column'; @@ -41,10 +41,12 @@ const hoveredContentStyle = css` color: ${props => props.theme.eui.euiColorFullShade}; `; -const TimestampColumnContent = LogEntryColumnContent.extend.attrs<{ +interface TimestampColumnContentProps { isHovered: boolean; isHighlighted: boolean; -}>({})` +} + +const TimestampColumnContent = euiStyled(LogEntryColumnContent)` color: ${props => props.theme.eui.euiColorDarkShade}; overflow: hidden; text-overflow: clip; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index fc3c8b3bf2b31..674c3f59ce957 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -179,7 +179,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent< /> {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => ( - + css` default: return props.theme.eui.euiFontSize; } - }} + }}; line-height: ${props => props.theme.eui.euiLineHeight}; `; @@ -59,7 +59,7 @@ export const useMeasuredCharacterDimensions = (scale: TextScale) => { const CharacterDimensionsProbe = useMemo( () => () => ( - + X ), @@ -72,11 +72,13 @@ export const useMeasuredCharacterDimensions = (scale: TextScale) => { }; }; -const MonospaceCharacterDimensionsProbe = euiStyled.div.attrs<{ +interface MonospaceCharacterDimensionsProbe { scale: TextScale; -}>({ +} + +const MonospaceCharacterDimensionsProbe = euiStyled.div.attrs(() => ({ 'aria-hidden': true, -})` +}))` visibility: hidden; position: absolute; height: auto; @@ -84,5 +86,5 @@ const MonospaceCharacterDimensionsProbe = euiStyled.div.attrs<{ padding: 0; margin: 0; - ${props => monospaceTextStyle(props.scale)} + ${props => monospaceTextStyle(props.scale)}; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx index 62db9d517c9d2..6daa942be78c8 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx @@ -247,10 +247,7 @@ export class VerticalScrollPanel extends React.PureComponent< style={{ height, width: width + scrollbarOffset }} scrollbarOffset={scrollbarOffset} onScroll={this.handleScroll} - innerRef={ - /* workaround for missing RefObject support in styled-components typings */ - this.scrollRef as any - } + ref={this.scrollRef} > {typeof children === 'function' ? children(this.registerChild) : null} @@ -258,7 +255,11 @@ export class VerticalScrollPanel extends React.PureComponent< } } -const ScrollPanelWrapper = euiStyled.div.attrs<{ scrollbarOffset?: number }>({})` +interface ScrollPanelWrapperProps { + scrollbarOffset?: number; +} + +const ScrollPanelWrapper = euiStyled.div` overflow-x: hidden; overflow-y: scroll; position: relative; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx index 1353b065bc444..a851f8380b915 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; @@ -12,6 +11,7 @@ import { StaticIndexPattern } from 'ui/index_patterns'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../autocomplete_field'; import { isDisplayable } from '../../utils/is_displayable'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: StaticIndexPattern; @@ -21,7 +21,7 @@ interface Props { function validateQuery(query: string) { try { - fromKueryExpression(query); + esKuery.fromKueryExpression(query); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx index fe3c930f9e08e..79785c11a3ebe 100644 --- a/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx +++ b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx @@ -25,10 +25,9 @@ const Nav = euiStyled.nav` background: ${props => props.theme.eui.euiColorEmptyShade}; border-bottom: ${props => props.theme.eui.euiBorderThin}; padding: ${props => - `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`} - + `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`}; .euiTabs { padding-left: 3px; margin-left: -3px; - } + }; `; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/gradient_legend.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/gradient_legend.tsx index 37a4b4bc563a0..3dcc40818b4d5 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/gradient_legend.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/gradient_legend.tsx @@ -35,7 +35,7 @@ const createTickRender = (bounds: InfraWaffleMapBounds, formatter: InfraFormatte ); }; -export const GradientLegend: React.SFC = ({ legend, bounds, formatter }) => { +export const GradientLegend: React.FC = ({ legend, bounds, formatter }) => { const maxValue = legend.rules.reduce((acc, rule) => { return acc < rule.value ? rule.value : acc; }, 0); diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_name.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_name.tsx index 511b41d91dd7b..731bcdd52a98e 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_name.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_name.tsx @@ -52,7 +52,7 @@ export class GroupName extends React.PureComponent { const GroupNameContainer = euiStyled.div` position: relative; - text-align: center + text-align: center; font-size: 16px; margin-bottom: 5px; top: 20px; @@ -65,7 +65,7 @@ interface InnerProps { isChild?: boolean; } -const Inner = euiStyled('div')` +const Inner = euiStyled.div` border: 1px solid ${props => props.theme.eui.euiBorderColor}; background-color: ${props => props.isChild ? props.theme.eui.euiColorLightestShade : props.theme.eui.euiColorEmptyShade}; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx index eaf0ea5e81d57..3f456c3c8d406 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx @@ -26,7 +26,7 @@ interface Props { timeRange: InfraTimerangeInput; } -export const GroupOfGroups: React.SFC = props => { +export const GroupOfGroups: React.FC = props => { return ( @@ -51,7 +51,7 @@ export const GroupOfGroups: React.SFC = props => { const GroupOfGroupsContainer = euiStyled.div` margin: 0 10px; - width: 100% + width: 100%; `; const Groups = euiStyled.div` diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx index 04011904cdbba..bc7d31a301496 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx @@ -27,7 +27,7 @@ interface Props { timeRange: InfraTimerangeInput; } -export const GroupOfNodes: React.SFC = ({ +export const GroupOfNodes: React.FC = ({ group, options, formatter, diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/legend.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/legend.tsx index ee403aa59fe13..c7f647449606e 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/legend.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/legend.tsx @@ -24,7 +24,7 @@ interface LegendControlOptions { bounds: InfraWaffleMapBounds; } -export const Legend: React.SFC = ({ dataBounds, legend, bounds, formatter }) => { +export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }) => { return ( diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx index f15bfff5d283e..ed7db4fe3dfe1 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx @@ -30,7 +30,7 @@ interface Props { dataBounds: InfraWaffleMapBounds; } -export const Map: React.SFC = ({ +export const Map: React.FC = ({ nodes, options, timeRange, @@ -46,10 +46,7 @@ export const Map: React.SFC = ({ {({ measureRef, content: { width = 0, height = 0 } }) => { const groupsWithLayout = applyWaffleMapLayout(map, width, height); return ( - measureRef(el)} - data-test-subj="waffleMap" - > + measureRef(el)} data-test-subj="waffleMap"> {groupsWithLayout.map(group => { if (isWaffleMapGroupWithGroups(group)) { diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx index 5839d070248cd..8f09a3fdca9cf 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx @@ -116,7 +116,7 @@ interface ColorProps { color: string; } -const SquareOuter = euiStyled('div')` +const SquareOuter = euiStyled.div` position: absolute; top: 4px; left: 4px; @@ -127,7 +127,7 @@ const SquareOuter = euiStyled('div')` box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); `; -const SquareInner = euiStyled('div')` +const SquareInner = euiStyled.div` cursor: pointer; position: absolute; top: 0; @@ -161,7 +161,7 @@ const ValueInner = euiStyled.button` } `; -const SquareTextContent = euiStyled('div')` +const SquareTextContent = euiStyled.div` text-align: center; width: 100%; overflow: hidden; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/steps_legend.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/steps_legend.tsx index 2b55d5652ec4e..e251720795074 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/steps_legend.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/steps_legend.tsx @@ -43,7 +43,7 @@ const createStep = (formatter: InfraFormatter) => (rule: InfraWaffleMapStepRule, ); }; -export const StepLegend: React.SFC = ({ legend, formatter }) => { +export const StepLegend: React.FC = ({ legend, formatter }) => { return {legend.rules.map(createStep(formatter))}; }; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts new file mode 100644 index 0000000000000..440ee10e4223d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { kfetch } from 'ui/kfetch'; + +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, +} from '../../../../../common/http_api'; + +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; + +export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { + const response = await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + body: JSON.stringify( + validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) + ), + }); + + return pipe( + validationIndicesResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index 163f0e39d1228..0f386f416b866 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'fetchingJobStatuses' }); return await callJobsSummaryAPI(spaceId, sourceId); }, - onResolve: response => { - dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId }); + onResolve: jobResponse => { + dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); }, onReject: err => { dispatch({ type: 'failedFetchingJobStatuses' }); @@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({ setup: setupMlModule, setupMlModuleRequest, setupStatus: statusState.setupStatus, + timestampField: timeField, viewSetupForReconfiguration, viewSetupForUpdate, viewResults, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 7942657018455..c965c50bedccc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { isExampleDataIndex } from '../../../../common/log_analysis'; +import { + ValidationIndicesError, + ValidationIndicesResponsePayload, +} from '../../../../common/http_api'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callIndexPatternsValidate } from './api/index_patterns_validate'; type SetupHandler = ( indices: string[], @@ -14,42 +20,75 @@ type SetupHandler = ( endTime: number | undefined ) => void; +export type ValidationIndicesUIError = + | ValidationIndicesError + | { error: 'NETWORK_ERROR' } + | { error: 'TOO_FEW_SELECTED_INDICES' }; + +export interface ValidatedIndex { + index: string; + errors: ValidationIndicesError[]; + isSelected: boolean; +} + interface AnalysisSetupStateArguments { availableIndices: string[]; cleanupAndSetupModule: SetupHandler; setupModule: SetupHandler; + timestampField: string; } -type IndicesSelection = Record; - -type ValidationErrors = 'TOO_FEW_SELECTED_INDICES'; - const fourWeeksInMs = 86400000 * 7 * 4; export const useAnalysisSetupState = ({ availableIndices, cleanupAndSetupModule, setupModule, + timestampField, }: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - const [selectedIndices, setSelectedIndices] = useState( - availableIndices.reduce( - (indexMap, indexName) => ({ - ...indexMap, - [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)), - }), - {} - ) + // Prepare the validation + const [validatedIndices, setValidatedIndices] = useState( + availableIndices.map(index => ({ + index, + errors: [], + isSelected: false, + })) + ); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callIndexPatternsValidate(timestampField, availableIndices); + }, + onResolve: ({ data }: ValidationIndicesResponsePayload) => { + setValidatedIndices( + availableIndices.map(index => { + const errors = data.errors.filter(error => error.index === index); + return { + index, + errors, + isSelected: errors.length === 0 && !isExampleDataIndex(index), + }; + }) + ); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [availableIndices, timestampField] ); + useEffect(() => { + validateIndices(); + }, [validateIndices]); + const selectedIndexNames = useMemo( - () => - Object.entries(selectedIndices) - .filter(([_indexName, isSelected]) => isSelected) - .map(([indexName]) => indexName), - [selectedIndices] + () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + [validatedIndices] ); const setup = useCallback(() => { @@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({ return cleanupAndSetupModule(selectedIndexNames, startTime, endTime); }, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]); - const validationErrors: ValidationErrors[] = useMemo( + const isValidating = useMemo( () => - Object.values(selectedIndices).some(isSelected => isSelected) - ? [] - : ['TOO_FEW_SELECTED_INDICES' as const], - [selectedIndices] + validateIndicesRequest.state === 'pending' || + validateIndicesRequest.state === 'uninitialized', + [validateIndicesRequest.state] ); + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + if (validateIndicesRequest.state === 'rejected') { + return [{ error: 'NETWORK_ERROR' }]; + } + + if (selectedIndexNames.length === 0) { + return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; + } + + return validatedIndices.reduce((errors, index) => { + return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + }, []); + }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + return { cleanupAndSetup, endTime, + isValidating, selectedIndexNames, - selectedIndices, setEndTime, - setSelectedIndices, setStartTime, setup, startTime, + validatedIndices, + setValidatedIndices, validationErrors, }; }; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_log_filter.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/with_log_filter.tsx index 77b41f8a0b7e2..e06e83f08680c 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_log_filter.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_log_filter.tsx @@ -63,7 +63,7 @@ type LogFilterUrlState = ReturnType = ({ indexPattern }) => ( +export const WithLogFilterUrlState: React.FC = ({ indexPattern }) => ( {({ applyFilterQuery, filterQuery }) => ( = ({ +export const WithWaffleFilterUrlState: React.FC = ({ indexPattern, }) => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/error.tsx b/x-pack/legacy/plugins/infra/public/pages/error.tsx index 0c9431bf501da..b525dd9b165ba 100644 --- a/x-pack/legacy/plugins/infra/public/pages/error.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/error.tsx @@ -28,7 +28,7 @@ interface Props { message: string; } -export const Error: React.SFC = ({ message }) => { +export const Error: React.FC = ({ message }) => { return (
@@ -39,7 +39,7 @@ export const Error: React.SFC = ({ message }) => { ); }; -export const ErrorPageBody: React.SFC<{ message: string }> = ({ message }) => { +export const ErrorPageBody: React.FC<{ message: string }> = ({ message }) => { return ( diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index 6affcae1805b3..fe48fcc62f77d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -41,10 +41,9 @@ export const InfrastructurePage = injectUICapabilities(
( +export const SnapshotPageContent: React.FC = () => ( {({ configuration, createDerivedIndexPattern, sourceId }) => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/link_to.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/link_to.tsx index 1318e9ca6c2c5..1e62af07224f1 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/link_to.tsx @@ -16,7 +16,7 @@ interface LinkToPageProps { match: RouteMatch<{}>; } -export const LinkToPage: React.SFC = props => ( +export const LinkToPage: React.FC = props => ( { lastSetupErrorMessages, setup, setupStatus, + timestampField, viewResults, } = useContext(LogAnalysisJobs.Context); @@ -61,6 +62,7 @@ export const AnalysisPageContent = () => { errorMessages={lastSetupErrorMessages} setup={setup} setupStatus={setupStatus} + timestampField={timestampField} viewResults={viewResults} /> ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx index 097cccf5dca33..7ae174c4a7899 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -34,6 +34,7 @@ interface AnalysisSetupContentProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent { useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); @@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx index defcefd69a7ab..585a65b9ad1c8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -4,37 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; - -export type IndicesSelection = Record; - -export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; +import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ - indices: IndicesSelection; - onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; -}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => { + indices: ValidatedIndex[]; + isValidating: boolean; + onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void; + valid: boolean; +}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => { + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent) => { + onChangeSelectedIndices( + indices.map(index => { + const checkbox = event.currentTarget; + return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + }) + ); + }, + [indices, onChangeSelectedIndices] + ); + const choices = useMemo( () => - Object.keys(indices).map(indexName => ({ - id: indexName, - label: {indexName}, - })), - [indices] - ); + indices.map(index => { + const validIndex = index.errors.length === 0; + const checkbox = ( + {index.index}} + onChange={handleCheckboxChange} + checked={index.isSelected} + disabled={!validIndex} + /> + ); - const handleCheckboxGroupChange = useCallback( - indexName => { - onChangeSelectedIndices({ - ...indices, - [indexName]: !indices[indexName], - }); - }, - [indices, onChangeSelectedIndices] + return validIndex ? ( + checkbox + ) : ( +
+ {checkbox} +
+ ); + }), + [indices] ); return ( @@ -53,20 +74,17 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ /> } > - 0} - label={indicesSelectionLabel} - labelType="legend" - > - - + + + <>{choices} + + ); }; @@ -75,14 +93,50 @@ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesS defaultMessage: 'Indices', }); -const formatValidationError = (validationError: IndicesValidationError) => { - switch (validationError) { - case 'TOO_FEW_SELECTED_INDICES': - return i18n.translate( - 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription', - { - defaultMessage: 'Select at least one index name.', - } - ); - } +const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { + return errors.map(error => { + switch (error.error) { + case 'INDEX_NOT_FOUND': + return ( +

+ {error.index} }} + /> +

+ ); + + case 'FIELD_NOT_FOUND': + return ( +

+ {error.index}, + field: {error.field}, + }} + /> +

+ ); + + case 'FIELD_NOT_VALID': + return ( +

+ {error.index}, + field: {error.field}, + }} + /> +

+ ); + + default: + return ''; + } + }); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx index 929fba26f2323..3b5497fb91864 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; -import { - AnalysisSetupIndicesForm, - IndicesSelection, - IndicesValidationError, -} from './analysis_setup_indices_form'; +import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; setEndTime: (endTime: number | undefined) => void; startTime: number | undefined; endTime: number | undefined; - selectedIndices: IndicesSelection; - setSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; + isValidating: boolean; + validatedIndices: ValidatedIndex[]; + setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void; + validationErrors?: ValidationIndicesUIError[]; } export const InitialConfigurationStep: React.FunctionComponent = ({ @@ -29,16 +32,11 @@ export const InitialConfigurationStep: React.FunctionComponent { - const indicesFormValidationErrors = useMemo( - () => - validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'), - [validationErrors] - ); - return ( <> @@ -50,11 +48,63 @@ export const InitialConfigurationStep: React.FunctionComponent + + ); }; + +const errorCalloutTitle = i18n.translate( + 'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle', + { + defaultMessage: 'Your index configuration is not valid', + } +); + +const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { + if (errors.length === 0) { + return null; + } + + return ( + <> + +
    + {errors.map((error, i) => ( +
  • {formatValidationError(error)}
  • + ))} +
+
+ + + ); +}; + +const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { + switch (error.error) { + case 'NETWORK_ERROR': + return ( + + ); + + case 'TOO_FEW_SELECTED_INDICES': + return ( + + ); + + default: + return ''; + } +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx index 27fc8a83bc086..978e45e26b733 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx @@ -60,8 +60,8 @@ export const ProcessStep: React.FunctionComponent = ({ defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist." /> - {errorMessages.map(errorMessage => ( - + {errorMessages.map((errorMessage, i) => ( + {errorMessage} ))} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx index aebb44d4c9372..4643516e73fac 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx @@ -25,6 +25,7 @@ interface AnalysisSetupStepsProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -34,6 +35,7 @@ export const AnalysisSetupSteps: React.FunctionComponent { const { @@ -43,13 +45,15 @@ export const AnalysisSetupSteps: React.FunctionComponent ), diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index 895d95b1471a0..1630de11bbdff 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -64,7 +64,7 @@ export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPag - +
{ {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => { return ( - + {({ buckets }) => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index 643d943273a81..00be769d572ac 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -151,7 +151,7 @@ export const MetricDetail = withMetricPageProviders( {({ measureRef, bounds: { width = 0 } }) => { const w = width ? `${width}px` : `100%`; return ( - + diff --git a/x-pack/legacy/plugins/infra/public/routes.tsx b/x-pack/legacy/plugins/infra/public/routes.tsx index 6e5ec8ea560b4..9dedc612bbe54 100644 --- a/x-pack/legacy/plugins/infra/public/routes.tsx +++ b/x-pack/legacy/plugins/infra/public/routes.tsx @@ -22,7 +22,7 @@ interface RouterProps { uiCapabilities: UICapabilities; } -const PageRouterComponent: React.SFC = ({ history, uiCapabilities }) => { +const PageRouterComponent: React.FC = ({ history, uiCapabilities }) => { return ( diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts index 069f631b9c026..f17f7be4defe9 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts @@ -5,10 +5,8 @@ */ import { createSelector } from 'reselect'; - -import { fromKueryExpression } from '@kbn/es-query'; - import { LogFilterState } from './reducer'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery ? state.filterQuery.query : null; @@ -23,7 +21,7 @@ export const selectIsLogFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts index 7d518b5e20f2d..0acce82950f77 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts @@ -6,8 +6,7 @@ import { createSelector } from 'reselect'; -import { fromKueryExpression } from '@kbn/es-query'; - +import { esKuery } from '../../../../../../../../src/plugins/data/public'; import { WaffleFilterState } from './reducer'; export const selectWaffleFilterQuery = (state: WaffleFilterState) => @@ -23,7 +22,7 @@ export const selectIsWaffleFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/utils/kuery.ts b/x-pack/legacy/plugins/infra/public/utils/kuery.ts index 4a767f2777512..2e793d53b4622 100644 --- a/x-pack/legacy/plugins/infra/public/utils/kuery.ts +++ b/x-pack/legacy/plugins/infra/public/utils/kuery.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esKuery } from '../../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, @@ -13,7 +13,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 98536f4c85d36..0093a6c21af57 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -13,7 +13,10 @@ import { createSnapshotResolvers } from './graphql/snapshot'; import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; -import { initLogAnalysisGetLogEntryRateRoute } from './routes/log_analysis'; +import { + initLogAnalysisGetLogEntryRateRoute, + initIndexPatternsValidateRoute, +} from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; @@ -33,6 +36,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); + initIndexPatternsValidateRoute(libs); initMetricExplorerRoute(libs); initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 48ef846ec5275..91bcd6be95a75 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -13,11 +13,8 @@ import { UsageCollector } from './usage/usage_collector'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export interface KbnServer extends Server { - usage: any; -} - -export const initServerWithKibana = (kbnServer: KbnServer) => { +export const initServerWithKibana = (kbnServer: Server) => { + const { usageCollection } = kbnServer.newPlatform.setup.plugins; const libs = compose(kbnServer); initInfraServer(libs); @@ -27,7 +24,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { ); // Register a function with server to manage the collection of usage stats - kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); + UsageCollector.registerUsageCollector(usageCollection); const xpackMainPlugin = kbnServer.plugins.xpack_main; xpackMainPlugin.registerFeature({ diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f0d26e5f5869f..63fded49d8222 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -45,7 +45,12 @@ export interface InfraBackendFrameworkAdapter { ): Promise; callWithRequest( req: InfraFrameworkRequest, - method: 'indices.getAlias' | 'indices.get', + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'indices.get', options?: object ): Promise; callWithRequest( @@ -137,14 +142,32 @@ export interface InfraDatabaseMultiResponse extends InfraDatab } export interface InfraDatabaseFieldCapsResponse extends InfraDatabaseResponse { + indices: string[]; fields: InfraFieldsResponse; } +export interface InfraDatabaseGetIndicesAliasResponse { + [indexName: string]: { + aliases: { + [aliasName: string]: any; + }; + }; +} + export interface InfraDatabaseGetIndicesResponse { [indexName: string]: { aliases: { [aliasName: string]: any; }; + mappings: { + _meta: object; + dynamic_templates: any[]; + date_detection: boolean; + properties: { + [fieldName: string]: any; + }; + }; + settings: { index: object }; }; } diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts index 38684cb22e237..7364d167efe47 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './index_patterns'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts new file mode 100644 index 0000000000000..a85e119e7318a --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './validate'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts new file mode 100644 index 0000000000000..0a369adb7ca29 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 Boom from 'boom'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, + ValidationIndicesError, +} from '../../../../common/http_api'; + +import { throwErrors } from '../../../../common/runtime_types'; + +const partitionField = 'event.dataset'; + +export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { + framework.registerRoute({ + method: 'POST', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + handler: async (req, res) => { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(req.payload), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; + + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { + index, + fields: `${timestampField},${partitionField}`, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', + index, + }); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; + + if (fieldMetadata === undefined) { + errors.push({ + error: 'FIELD_NOT_FOUND', + index, + field, + }); + } else { + const fieldTypes = Object.keys(fieldMetadata); + + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + + return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); + }, + }); +}; diff --git a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts index 018c903009bbe..601beddc0a2db 100644 --- a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts +++ b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { InfraNodeType } from '../graphql/types'; -import { KbnServer } from '../kibana.index'; - const KIBANA_REPORTING_TYPE = 'infraops'; interface InfraopsSum { @@ -17,10 +16,13 @@ interface InfraopsSum { } export class UsageCollector { - public static getUsageCollector(server: KbnServer) { - const { collectorSet } = server.usage; + public static registerUsageCollector(usageCollection: UsageCollectionSetup): void { + const collector = UsageCollector.getUsageCollector(usageCollection); + usageCollection.registerCollector(collector); + } - return collectorSet.makeUsageCollector({ + public static getUsageCollector(usageCollection: UsageCollectionSetup) { + return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady: () => true, fetch: async () => { diff --git a/x-pack/legacy/plugins/infra/types/eui.d.ts b/x-pack/legacy/plugins/infra/types/eui.d.ts index ef58bfcb2fa04..7cf0a91e88c1f 100644 --- a/x-pack/legacy/plugins/infra/types/eui.d.ts +++ b/x-pack/legacy/plugins/infra/types/eui.d.ts @@ -41,7 +41,7 @@ declare module '@elastic/eui' { toggleOpenOnMobile?: () => void; isOpenOnMobile?: boolean; }; - export const EuiSideNav: React.SFC; + export const EuiSideNav: React.FC; type EuiErrorBoundaryProps = CommonProps & { children: React.ReactNode; @@ -53,10 +53,6 @@ declare module '@elastic/eui' { sizes: EuiSizesResponsive[]; }; - export const EuiHideFor: React.SFC; - - export const EuiShowFor: React.SFC; - type EuiInMemoryTableProps = CommonProps & { items?: any; columns?: any; @@ -72,6 +68,7 @@ declare module '@elastic/eui' { rowProps?: any; cellProps?: any; responsive?: boolean; + itemIdToExpandedRowMap?: any; }; - export const EuiInMemoryTable: React.SFC; + export const EuiInMemoryTable: React.FC; } diff --git a/x-pack/legacy/plugins/infra/types/eui_experimental.d.ts b/x-pack/legacy/plugins/infra/types/eui_experimental.d.ts index 16320531f42b3..6b01bd8066adc 100644 --- a/x-pack/legacy/plugins/infra/types/eui_experimental.d.ts +++ b/x-pack/legacy/plugins/infra/types/eui_experimental.d.ts @@ -20,7 +20,7 @@ declare module '@elastic/eui/lib/experimental' { animateData?: boolean; marginLeft?: number; }; - export const EuiSeriesChart: React.SFC; + export const EuiSeriesChart: React.FC; type EuiSeriesProps = CommonProps & { data: Array<{ x: number; y: number; y0?: number }>; @@ -29,21 +29,21 @@ declare module '@elastic/eui/lib/experimental' { color?: string; marginLeft?: number; }; - export const EuiLineSeries: React.SFC; - export const EuiAreaSeries: React.SFC; - export const EuiBarSeries: React.SFC; + export const EuiLineSeries: React.FC; + export const EuiAreaSeries: React.FC; + export const EuiBarSeries: React.FC; type EuiYAxisProps = CommonProps & { tickFormat: (value: number) => string; marginLeft?: number; }; - export const EuiYAxis: React.SFC; + export const EuiYAxis: React.FC; type EuiXAxisProps = CommonProps & { tickFormat?: (value: number) => string; marginLeft?: number; }; - export const EuiXAxis: React.SFC; + export const EuiXAxis: React.FC; export interface EuiDataPoint { seriesIndex: number; @@ -66,5 +66,5 @@ declare module '@elastic/eui/lib/experimental' { titleFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue | undefined; itemsFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue[]; }; - export const EuiCrosshairX: React.SFC; + export const EuiCrosshairX: React.FC; } diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js index 8a20337317cfb..6eae233cf5dea 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { getSuggestionsProvider } from '../field'; import indexPatternResponse from '../__fixtures__/index_pattern_response.json'; -import { isFilterable } from 'ui/index_patterns'; +import { isFilterable } from '../../../../../../../src/plugins/data/public'; describe('Kuery field suggestions', function () { let indexPattern; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js index 3d7e1979d224b..1a00c668833aa 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js @@ -7,7 +7,7 @@ import React from 'react'; import { flatten } from 'lodash'; import { escapeKuery } from './escape_kuery'; import { sortPrefixFirst } from 'ui/utils/sort_prefix_first'; -import { isFilterable } from 'ui/index_patterns'; +import { isFilterable } from '../../../../../../src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js index 8a82194470ace..7b0e42283d5f5 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js @@ -5,11 +5,11 @@ */ import { flatten, mapValues, uniq } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { getSuggestionsProvider as field } from './field'; import { getSuggestionsProvider as value } from './value'; import { getSuggestionsProvider as operator } from './operator'; import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; @@ -27,7 +27,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { let cursorNode; try { - cursorNode = fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); } catch (e) { cursorNode = {}; } diff --git a/x-pack/legacy/plugins/lens/common/constants.ts b/x-pack/legacy/plugins/lens/common/constants.ts index 787a348a788b8..c2eed1940fa1a 100644 --- a/x-pack/legacy/plugins/lens/common/constants.ts +++ b/x-pack/legacy/plugins/lens/common/constants.ts @@ -6,9 +6,13 @@ export const PLUGIN_ID = 'lens'; -export const BASE_APP_URL = '/app/lens'; +export const BASE_APP_URL = '/app/kibana'; export const BASE_API_URL = '/api/lens'; +export function getBasePath() { + return `${BASE_APP_URL}#/lens`; +} + export function getEditPath(id: string) { - return `${BASE_APP_URL}#/edit/${encodeURIComponent(id)}`; + return `${BASE_APP_URL}#/lens/edit/${encodeURIComponent(id)}`; } diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 20f92ebbe0654..a79b9907f6437 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -12,7 +12,7 @@ import mappings from './mappings.json'; import { PLUGIN_ID, getEditPath } from './common'; import { lensServerPlugin } from './server'; -const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; +export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const lens: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ @@ -26,10 +26,11 @@ export const lens: LegacyPluginInitializer = kibana => { app: { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, description: 'Explore and visualize data.', - main: `plugins/${PLUGIN_ID}/index`, + main: `plugins/${PLUGIN_ID}/redirect`, listed: false, }, - embeddableFactories: ['plugins/lens/register_embeddable'], + visualize: [`plugins/${PLUGIN_ID}/legacy`], + embeddableFactories: [`plugins/${PLUGIN_ID}/legacy`], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, visTypes: ['plugins/lens/register_vis_type_alias'], @@ -57,10 +58,12 @@ export const lens: LegacyPluginInitializer = kibana => { // Set up with the new platform plugin lifecycle API. const plugin = lensServerPlugin(); + const { usageCollection } = server.newPlatform.setup.plugins; + plugin.setup(kbnServer.newPlatform.setup.core, { + usageCollection, // Legacy APIs savedObjects: server.savedObjects, - usage: server.usage, config: server.config(), server, }); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 3fcb6609f28f1..ce05af46ade66 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -99,6 +99,12 @@ describe('Lens App', () => { data: { query: { filterManager: createMockFilterManager(), + timefilter: { + timefilter: { + getTime: jest.fn(() => ({ from: 'now-7d', to: 'now' })), + setTime: jest.fn(), + }, + }, }, }, dataShim: { @@ -109,7 +115,6 @@ describe('Lens App', () => { }), }, }, - timefilter: { history: {} }, }, storage: { get: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index fc5088c1271ad..c29b0df9ee9fa 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -import { CoreStart, NotificationsStart } from 'src/core/public'; +import { AppMountContext, NotificationsStart } from 'src/core/public'; import { DataStart, IndexPattern as IndexPatternInstance, @@ -55,40 +55,42 @@ export function App({ }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; - core: CoreStart; + core: AppMountContext['core']; dataShim: DataStart; storage: IStorageWrapper; docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; }) { - const timeDefaults = core.uiSettings.get('timepicker:timeDefaults'); const language = storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); - const [state, setState] = useState({ - isLoading: !!docId, - isSaveModalVisible: false, - indexPatternsForTopNav: [], - query: { query: '', language }, - dateRange: { - fromDate: timeDefaults.from, - toDate: timeDefaults.to, - }, - filters: [], + const [state, setState] = useState(() => { + const currentRange = data.query.timefilter.timefilter.getTime(); + return { + isLoading: !!docId, + isSaveModalVisible: false, + indexPatternsForTopNav: [], + query: { query: '', language }, + dateRange: { + fromDate: currentRange.from, + toDate: currentRange.to, + }, + filters: [], + }; }); const { lastKnownDoc } = state; useEffect(() => { - const subscription = data.query.filterManager.getUpdates$().subscribe({ + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); trackUiEvent('app_filters_updated'); }, }); return () => { - subscription.unsubscribe(); + filterSubscription.unsubscribe(); }; }, []); @@ -199,6 +201,7 @@ export function App({ dateRange.from !== state.dateRange.fromDate || dateRange.to !== state.dateRange.toDate ) { + data.query.timefilter.timefilter.setTime(dateRange); trackUiEvent('app_date_change'); } else { trackUiEvent('app_query_change'); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 56c19ea2bb9f2..93f5928f58aa1 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -4,18 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +// Used for kibana_context function +import 'uiExports/savedObjectTypes'; + import React from 'react'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; -import chrome from 'ui/chrome'; -import { CoreSetup, CoreStart } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; -import { start as dataShimStart } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; +import { addHelpMenuToAppChrome } from '../help_menu_util'; import { SavedObjectIndexStore } from '../persistence'; import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; @@ -24,25 +32,35 @@ import { datatableVisualizationStop, } from '../datatable_visualization_plugin'; import { App } from './app'; -import { EditorFrameInstance } from '../types'; import { LensReportManager, setReportManager, stopReportManager, trackUiEvent, } from '../lens_ui_telemetry'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../index'; +import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; +import { EditorFrameStart } from '../types'; + +export interface LensPluginSetupDependencies { + kibana_legacy: KibanaLegacySetup; +} export interface LensPluginStartDependencies { data: DataPublicPluginStart; dataShim: DataStart; } export class AppPlugin { - private instance: EditorFrameInstance | null = null; - private store: SavedObjectIndexStore | null = null; + private startDependencies: { + data: DataPublicPluginStart; + dataShim: DataStart; + savedObjectsClient: SavedObjectsClientContract; + editorFrame: EditorFrameStart; + } | null = null; constructor() {} - setup(core: CoreSetup, plugins: {}) { + setup(core: CoreSetup, { kibana_legacy }: LensPluginSetupDependencies) { // TODO: These plugins should not be called from the top level, but since this is the // entry point to the app we have no choice until the new platform is ready const indexPattern = indexPatternDatasourceSetup(); @@ -50,77 +68,87 @@ export class AppPlugin { const xyVisualization = xyVisualizationSetup(); const metricVisualization = metricVisualizationSetup(); const editorFrameSetupInterface = editorFrameSetup(); - this.store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient()); editorFrameSetupInterface.registerVisualization(xyVisualization); editorFrameSetupInterface.registerVisualization(datatableVisualization); editorFrameSetupInterface.registerVisualization(metricVisualization); editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern); - } - - start(core: CoreStart, { data, dataShim }: LensPluginStartDependencies) { - if (this.store === null) { - throw new Error('Start lifecycle called before setup lifecycle'); - } - - const store = this.store; - - const editorFrameStartInterface = editorFrameStart(); - this.instance = editorFrameStartInterface.createInstance({}); - - setReportManager( - new LensReportManager({ - storage: new Storage(localStorage), - http: core.http, - }) - ); + kibana_legacy.registerLegacyApp({ + id: 'lens', + title: NOT_INTERNATIONALIZED_PRODUCT_NAME, + mount: async (context, params) => { + if (this.startDependencies === null) { + throw new Error('mounted before start phase'); + } + const { data, dataShim, savedObjectsClient, editorFrame } = this.startDependencies; + addHelpMenuToAppChrome(context.core.chrome); + + const instance = editorFrame.createInstance({}); + + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + http: core.http, + }) + ); + + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); + return ( + { + if (!id) { + routeProps.history.push('/lens'); + } else { + routeProps.history.push(`/lens/edit/${id}`); + } + }} + /> + ); + }; + + function NotFound() { + trackUiEvent('loaded_404'); + return ; + } + render( + + + + + + + + + , + params.element + ); + return () => { + instance.unmount(); + unmountComponentAtNode(params.element); + }; + }, + }); + } - const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - trackUiEvent('loaded'); - return ( - { - if (!id) { - routeProps.history.push('/'); - } else { - routeProps.history.push(`/edit/${id}`); - } - }} - /> - ); + start({ savedObjects }: CoreStart, { data, dataShim }: LensPluginStartDependencies) { + this.startDependencies = { + data, + dataShim, + savedObjectsClient: savedObjects.client, + editorFrame: editorFrameStart(), }; - - function NotFound() { - trackUiEvent('loaded_404'); - return ; - } - - return ( - - - - - - - - - - ); } stop() { - if (this.instance) { - this.instance.unmount(); - } - stopReportManager(); // TODO this will be handled by the plugin platform itself @@ -131,10 +159,3 @@ export class AppPlugin { editorFrameStop(); } } - -const app = new AppPlugin(); - -export const appSetup = () => app.setup(npSetup.core, {}); -export const appStart = () => - app.start(npStart.core, { dataShim: dataShimStart, data: npStart.plugins.data }); -export const appStop = () => app.stop(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 354a5186db4c1..f7399255b2001 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -15,8 +15,8 @@ import { ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; import { - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../../../src/plugins/embeddable/public'; import { setup as dataSetup, @@ -36,13 +36,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: typeof dataSetup; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: typeof dataStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; expressions: ExpressionsStart; chrome: Chrome; } diff --git a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx b/x-pack/legacy/plugins/lens/public/help_menu_util.tsx index 30a05dbc38537..9ead31690e854 100644 --- a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx +++ b/x-pack/legacy/plugins/lens/public/help_menu_util.tsx @@ -4,55 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiHorizontalRule, EuiSpacer, EuiLink, EuiText, EuiIcon, EuiButton } from '@elastic/eui'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { Chrome } from 'ui/chrome'; +import { ChromeStart } from 'kibana/public'; -const docsPage = 'lens'; - -export function addHelpMenuToAppChrome(chrome: Chrome) { - chrome.helpExtension.set(domElement => { - render(, domElement); - return () => { - unmountComponentAtNode(domElement); - }; +export function addHelpMenuToAppChrome(chrome: ChromeStart) { + chrome.setHelpExtension({ + appName: 'Lens', + links: [ + { + linkType: 'documentation', + href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, + }, + { + linkType: 'github', + title: '[Lens]', + labels: ['Feature:Lens'], + }, + ], }); } - -function HelpMenu() { - return ( - <> - - {docsPage && ( - <> - - - - - - )} - - - -   - - {i18n.translate('xpack.lens.helpMenu.feedbackLinkText', { - defaultMessage: 'Provide feedback for the Lens application', - })} - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/legacy/plugins/lens/public/index.ts index 2a5422d4bb8a1..9f4141dbcae7d 100644 --- a/x-pack/legacy/plugins/lens/public/index.ts +++ b/x-pack/legacy/plugins/lens/public/index.ts @@ -5,36 +5,3 @@ */ export * from './types'; - -import 'ui/autoload/all'; -// Used to run esaggs queries -import 'uiExports/fieldFormats'; -import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/interpreter'; -// Used for kibana_context function -import 'uiExports/savedObjectTypes'; - -import { render, unmountComponentAtNode } from 'react-dom'; -import { IScope } from 'angular'; -import chrome from 'ui/chrome'; -import { appStart, appSetup, appStop } from './app_plugin'; -import { PLUGIN_ID } from '../common'; -import { addHelpMenuToAppChrome } from './help_menu_util'; - -// TODO: Convert this to the "new platform" way of doing UI -function Root($scope: IScope, $element: JQLite) { - const el = $element[0]; - $scope.$on('$destroy', () => { - unmountComponentAtNode(el); - appStop(); - }); - - appSetup(); - addHelpMenuToAppChrome(chrome); - - return render(appStart(), el); -} - -chrome.setRootController(PLUGIN_ID, Root); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index affb1accbbef4..dc23df250ebd4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -17,7 +17,6 @@ import { EuiProgress } from '@elastic/eui'; import { documentField } from './document_field'; jest.mock('ui/new_platform'); -jest.mock('../../../../../../src/legacy/ui/public/registry/field_formats'); const initialState: IndexPatternPrivateState = { indexPatternRefs: [], diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx index 9956c0ec33061..1b49eb6bca7fa 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx @@ -10,63 +10,58 @@ import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; import { FieldItem, FieldItemProps } from './field_item'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { npStart } from 'ui/new_platform'; +import { FieldFormatsStart } from '../../../../../../src/plugins/data/public'; +import { IndexPattern } from './types'; jest.mock('ui/new_platform'); -// Formatter must be mocked to return a string, or the rendering will fail -jest.mock('../../../../../../src/legacy/ui/public/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn().mockReturnValue({ - convert: jest.fn().mockReturnValue((s: unknown) => JSON.stringify(s)), - }), - }, -})); - const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); -const indexPattern = { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], -}; - describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; + let indexPattern: IndexPattern; let core: ReturnType; beforeEach(() => { + indexPattern = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + } as IndexPattern; + core = coreMock.createSetup(); core.http.post.mockClear(); defaultProps = { @@ -87,6 +82,12 @@ describe('IndexPattern Field Item', () => { }, exists: true, }; + + npStart.plugins.data.fieldFormats = ({ + getDefaultInstance: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), + } as unknown) as FieldFormatsStart; }); it('should request field stats every time the button is clicked', async () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 3536ad8053891..20505107be122 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -7,48 +7,49 @@ import React, { useState } from 'react'; import DateMath from '@elastic/datemath'; import { + EuiButtonGroup, EuiFlexGroup, EuiFlexItem, - EuiProgress, - EuiPopover, - EuiLoadingSpinner, + EuiIconTip, EuiKeyboardAccessible, - EuiText, - EuiToolTip, - EuiButtonGroup, + EuiLoadingSpinner, + EuiPopover, EuiPopoverFooter, EuiPopoverTitle, - EuiIconTip, + EuiProgress, + EuiText, + EuiToolTip, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { - Chart, Axis, + BarSeries, + Chart, + DataSeriesColorsValues, getAxisId, getSpecId, - BarSeries, + niceTimeFormatter, Position, ScaleType, Settings, - DataSeriesColorsValues, TooltipType, - niceTimeFormatter, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { Query, + KBN_FIELD_TYPES, + ES_FIELD_TYPES, esFilters, esQuery, IIndexPattern, } from '../../../../../../src/plugins/data/public'; -// @ts-ignore -import { fieldFormats } from '../../../../../../src/legacy/ui/public/registry/field_formats'; import { DraggedField } from './indexpattern'; import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; -import { LensFieldIcon, getColorForDataType } from './lens_field_icon'; +import { getColorForDataType, LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { @@ -238,6 +239,7 @@ export function FieldItem(props: FieldItemProps) { } function FieldItemPopoverContents(props: State & FieldItemProps) { + const fieldFormats = npStart.plugins.data.fieldFormats; const { histogram, topValues, indexPattern, field, dateRange, core, sampledValues } = props; const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); @@ -289,7 +291,10 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { formatter = { convert: (data: unknown) => JSON.stringify(data) }; } } else { - formatter = fieldFormats.getDefaultInstance(field.type, field.esTypes); + formatter = fieldFormats.getDefaultInstance( + field.type as KBN_FIELD_TYPES, + field.esTypes as ES_FIELD_TYPES[] + ); } const euiButtonColor = @@ -370,7 +375,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { )}{' '} {fieldFormats - .getDefaultInstance('number', ['integer']) + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) .convert(props.totalDocuments)} {' '} {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index d2cf6261835fd..35e99fc4fe98d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -116,7 +116,11 @@ export function getDatasourceSuggestionsForField( } function getBucketOperation(field: IndexPatternField) { - return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); + // We allow numeric bucket types in some cases, but it's generally not the right suggestion, + // so we eliminate it here. + if (field.type !== 'number') { + return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); + } } function getExistingLayerSuggestionsForField( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index e57745c11fc69..7b21ef92ab82b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -135,6 +135,24 @@ describe('terms', () => { scale: 'ordinal', }); + expect( + termsOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + type: 'number', + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }) + ).toEqual({ + dataType: 'number', + isBucketed: true, + scale: 'ordinal', + }); + expect( termsOperation.getPossibleOperationForField({ aggregatable: true, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx index cb8893dd2f6dd..cd0dcc0b7e9ce 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx @@ -37,7 +37,7 @@ function isSortableByColumn(column: IndexPatternColumn) { } const DEFAULT_SIZE = 3; -const supportedTypes = new Set(['string', 'boolean', 'ip']); +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'terms'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 0161b93effc52..3602491c6eb2c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -228,6 +228,20 @@ describe('getOperationTypesForField', () => { it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ + Object { + "operationMetaData": Object { + "dataType": "number", + "isBucketed": true, + "scale": "ordinal", + }, + "operations": Array [ + Object { + "field": "bytes", + "operationType": "terms", + "type": "field", + }, + ], + }, Object { "operationMetaData": Object { "dataType": "string", diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts new file mode 100644 index 0000000000000..a39d73f187ece --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; +import { start as dataShimStart } from '../../../../../src/legacy/core_plugins/data/public/legacy'; + +export * from './types'; + +import { AppPlugin } from './app_plugin'; + +const app = new AppPlugin(); +app.setup(npSetup.core, npSetup.plugins); +app.start(npStart.core, { + ...npStart.plugins, + dataShim: dataShimStart, +}); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index ba1ac461161b1..9220c3ec75fad 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -9,7 +9,7 @@ import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { MetricConfig } from './types'; -import { FieldFormat } from 'ui/registry/field_formats'; +import { FieldFormat } from '../../../../../../src/plugins/data/public'; function sampleArgs() { const data: LensMultiTable = { diff --git a/x-pack/legacy/plugins/lens/public/redirect.ts b/x-pack/legacy/plugins/lens/public/redirect.ts new file mode 100644 index 0000000000000..25b0188214c5e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/redirect.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This file redirects lens urls starting with app/lens#... to their counterpart on app/kibana#lens/... to +// make sure it's compatible with the 7.5 release + +import { npSetup } from 'ui/new_platform'; +import chrome from 'ui/chrome'; + +chrome.setRootController('lens', () => { + // prefix the path in the hash with lens/ + const prefixedHashRoute = window.location.hash.replace(/^#\//, '#/lens/'); + + // redirect to the new lens url `app/kibana#/lens/...` + window.location.href = npSetup.core.http.basePath.prepend('/app/kibana' + prefixedHashRoute); +}); diff --git a/x-pack/legacy/plugins/lens/public/register_embeddable.ts b/x-pack/legacy/plugins/lens/public/register_embeddable.ts deleted file mode 100644 index f86cb91a0029e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/register_embeddable.ts +++ /dev/null @@ -1,20 +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 { indexPatternDatasourceSetup } from './indexpattern_plugin'; -import { xyVisualizationSetup } from './xy_visualization_plugin'; -import { editorFrameSetup, editorFrameStart } from './editor_frame_plugin'; -import { datatableVisualizationSetup } from './datatable_visualization_plugin'; -import { metricVisualizationSetup } from './metric_visualization_plugin'; - -// bootstrap shimmed plugins to register everything necessary (expression functions and embeddables). -// the new platform will take care of this once in place. -indexPatternDatasourceSetup(); -datatableVisualizationSetup(); -xyVisualizationSetup(); -metricVisualizationSetup(); -editorFrameSetup(); -editorFrameStart(); diff --git a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts index 562d0f0ef6135..185df12054a3c 100644 --- a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts +++ b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; import { visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public'; -import { BASE_APP_URL, getEditPath } from '../common'; +import { getBasePath, getEditPath } from '../common'; visualizations.types.registerAlias({ - aliasUrl: BASE_APP_URL, + aliasUrl: getBasePath(), name: 'lens', promotion: { description: i18n.translate('xpack.lens.visTypeAlias.promotion.description', { diff --git a/x-pack/legacy/plugins/lens/server/plugin.tsx b/x-pack/legacy/plugins/lens/server/plugin.tsx index a4c8e9b268df5..0223b90c37046 100644 --- a/x-pack/legacy/plugins/lens/server/plugin.tsx +++ b/x-pack/legacy/plugins/lens/server/plugin.tsx @@ -6,27 +6,22 @@ import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { Plugin, CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { setupRoutes } from './routes'; import { registerLensUsageCollector, initializeLensTelemetry } from './usage'; +export interface PluginSetupContract { + savedObjects: SavedObjectsLegacyService; + usageCollection: UsageCollectionSetup; + config: KibanaConfig; + server: Server; +} + export class LensServer implements Plugin<{}, {}, {}, {}> { - setup( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } - ) { + setup(core: CoreSetup, plugins: PluginSetupContract) { setupRoutes(core, plugins); - registerLensUsageCollector(core, plugins); - initializeLensTelemetry(core, plugins); + registerLensUsageCollector(plugins.usageCollection, plugins.server); + initializeLensTelemetry(core, plugins.server); return {}; } diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 94a7c8e0d85c1..274b72c33e59a 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -6,29 +6,17 @@ import moment from 'moment'; import { get } from 'lodash'; -import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; -import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { Server } from 'src/legacy/server/kbn_server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { LensUsage, LensTelemetryState } from './types'; -export function registerLensUsageCollector( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } -) { +export function registerLensUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { let isCollectorReady = false; async function determineIfTaskManagerIsReady() { let isReady = false; try { - isReady = await isTaskManagerReady(plugins.server); + isReady = await isTaskManagerReady(server); } catch (err) {} // eslint-disable-line if (isReady) { @@ -39,11 +27,11 @@ export function registerLensUsageCollector( } determineIfTaskManagerIsReady(); - const lensUsageCollector = plugins.usage.collectorSet.makeUsageCollector({ + const lensUsageCollector = usageCollection.makeUsageCollector({ type: 'lens', fetch: async (): Promise => { try { - const docs = await getLatestTaskState(plugins.server); + const docs = await getLatestTaskState(server); // get the accumulated state from the recurring task const state: LensTelemetryState = get(docs, '[0].state'); @@ -75,7 +63,8 @@ export function registerLensUsageCollector( }, isReady: () => isCollectorReady, }); - plugins.usage.collectorSet.register(lensUsageCollector); + + usageCollection.registerCollector(lensUsageCollector); } function addEvents(prevEvents: Record, newEvents: Record) { diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 03e085cc9e669..feb73538f44f0 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -39,12 +39,12 @@ type ClusterDeleteType = ( options?: CallClusterOptions ) => Promise; -export function initializeLensTelemetry(core: CoreSetup, { server }: { server: Server }) { - registerLensTelemetryTask(core, { server }); +export function initializeLensTelemetry(core: CoreSetup, server: Server) { + registerLensTelemetryTask(core, server); scheduleTasks(server); } -function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) { +function registerLensTelemetryTask(core: CoreSetup, server: Server) { const taskManager = server.plugins.task_manager; if (!taskManager) { diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap index d5be772af9441..03421e66c77f5 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddLicense component when license is active should display correct verbiage 1`] = `""`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `""`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `""`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index 8d91ddcc563df..8670b2c378dca 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

Extend trial
"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

Extend trial
"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

Extend trial
"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index 3b3cf35dcfd91..cb2a41dadbe9e 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 9812152dc6363..df82a820e9513 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index ade645d76cad9..3b2f887e13c87 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; export const EMS_CATALOGUE_PATH = 'ems/catalogue'; @@ -67,7 +68,7 @@ export const ZOOM_PRECISION = 2; export const ES_SIZE_LIMIT = 10000; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; -export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__'; +export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; @@ -114,3 +115,15 @@ export const METRIC_TYPE = { SUM: 'sum', UNIQUE_COUNT: 'cardinality', }; + +export const COUNT_AGG_TYPE = METRIC_TYPE.COUNT; +export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { + defaultMessage: 'count' +}); + +export const COUNT_PROP_NAME = 'doc_count'; + +export const STYLE_TYPE = { + 'STATIC': 'STATIC', + 'DYNAMIC': 'DYNAMIC' +}; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 739e98beec10f..c59fbe42a1754 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -101,12 +101,12 @@ export function maps(kibana) { init(server) { const mapsEnabled = server.config().get('xpack.maps.enabled'); - + const { usageCollection } = server.newPlatform.setup.plugins; if (!mapsEnabled) { server.log(['info', 'maps'], 'Maps app disabled by configuration'); return; } - initTelemetryCollection(server); + initTelemetryCollection(usageCollection, server); const xpackMainPlugin = server.plugins.xpack_main; let routesInitialized = false; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 0dbbbf18426a5..324e975675454 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -67,6 +67,9 @@ export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE'; export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE'; export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM'; export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR'; +export const SET_INTERACTIVE = 'SET_INTERACTIVE'; +export const DISABLE_TOOLTIP_CONTROL = 'DISABLE_TOOLTIP_CONTROL'; +export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY'; function getLayerLoadingCallbacks(dispatch, layerId) { return { @@ -732,9 +735,7 @@ export function clearMissingStyleProperties(layerId) { return; } - const dateFields = await targetLayer.getDateFields(); - const numberFields = await targetLayer.getNumberFields(); - const ordinalFields = [...dateFields, ...numberFields]; + const ordinalFields = await targetLayer.getOrdinalFields(); const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved(ordinalFields); if (hasChanges) { dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); @@ -816,3 +817,15 @@ export function updateDrawState(drawState) { }); }; } + +export function disableInteractive() { + return { type: SET_INTERACTIVE, disableInteractive: true }; +} + +export function disableTooltipControl() { + return { type: DISABLE_TOOLTIP_CONTROL, disableTooltipControl: true }; +} + +export function hideToolbarOverlay() { + return { type: HIDE_TOOLBAR_OVERLAY, hideToolbarOverlay: true }; +} diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 90d4ddbeb0092..2f34ffa660d6e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -1,4 +1,5 @@
+
+

{{screenTitle}}

diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 41c618d68a68e..b9354dd0a0ddd 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -66,6 +66,7 @@ const app = uiModules.get(MAP_APP_PATH, []); app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { const { filterManager } = npStart.plugins.data.query; const savedMap = $route.current.locals.map; + $scope.screenTitle = savedMap.title; let unsubscribe; let initialLayerListConfig; const $state = new AppState(); diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap index 968915905cd31..f37dfdd879c5b 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap @@ -22,7 +22,7 @@ exports[`Should remove selected fields from selectable 1`] = ` hasArrow={true} id="addTooltipFieldPopover" isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="m" > {this._renderContent()} diff --git a/x-pack/legacy/plugins/maps/public/components/help_menu.js b/x-pack/legacy/plugins/maps/public/components/help_menu.js deleted file mode 100644 index 34cb66eb7bd83..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/help_menu.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, PureComponent } from 'react'; -import { EuiButton, EuiHorizontalRule, EuiSpacer, EuiLink, EuiText, EuiIcon } from '@elastic/eui'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class HelpMenu extends PureComponent { - render() { - return ( - - - - - - - - -   - - {i18n.translate('xpack.maps.helpMenu.feedbackLinkText', { - defaultMessage: 'Provide feedback for the Maps application', - })} - - - - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js index f5a4b94072a4d..50cbbdb3b7180 100644 --- a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js +++ b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js @@ -30,35 +30,109 @@ const reorder = (list, startIndex, endIndex) => { return result; }; +const getProps = async field => { + return new Promise(async (resolve, reject) => { + try { + const label = await field.getLabel(); + const type = await field.getDataType(); + resolve({ + label: label, + type: type, + name: field.getName() + }); + } catch(e) { + reject(e); + } + }); +}; + export class TooltipSelector extends Component { + state = { + fieldProps: [], + selectedFieldProps: [] + }; + + constructor() { + super(); + this._isMounted = false; + this._previousFields = null; + this._previousSelectedTooltips = null; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldProps(); + this._loadTooltipFieldProps(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._loadTooltipFieldProps(); + this._loadFieldProps(); + } + + async _loadTooltipFieldProps() { + + if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) { + return; + } + + this._previousSelectedTooltips = this.props.tooltipFields; + const selectedProps = this.props.tooltipFields.map(getProps); + const selectedFieldProps = await Promise.all(selectedProps); + if (this._isMounted) { + this.setState({ selectedFieldProps }); + } + + } + + async _loadFieldProps() { + + if (!this.props.fields || this.props.fields === this._previousFields) { + return; + } + + this._previousFields = this.props.fields; + const props = this.props.fields.map(getProps); + const fieldProps = await Promise.all(props); + if (this._isMounted) { + this.setState({ fieldProps }); + } + + } + _getPropertyLabel = (propertyName) => { - if (!this.props.fields) { + if (!this.state.fieldProps.length) { return propertyName; } - - const field = this.props.fields.find(field => { + const prop = this.state.fieldProps.find((field) => { return field.name === propertyName; }); + return prop.label ? prop.label : propertyName; + } - return field && field.label - ? field.label - : propertyName; + _getTooltipProperties() { + return this.props.tooltipFields.map(field => field.getName()); } _onAdd = (properties) => { - if (!this.props.tooltipProperties) { + if (!this.props.tooltipFields) { this.props.onChange([...properties]); } else { - this.props.onChange([...this.props.tooltipProperties, ...properties]); + const existingProperties = this._getTooltipProperties(); + this.props.onChange([...existingProperties, ...properties]); } } _removeProperty = (index) => { - if (!this.props.tooltipProperties) { + if (!this.props.tooltipFields) { this.props.onChange([]); } else { - const tooltipProperties = [...this.props.tooltipProperties]; + const tooltipProperties = this._getTooltipProperties(); tooltipProperties.splice(index, 1); this.props.onChange(tooltipProperties); } @@ -70,11 +144,11 @@ export class TooltipSelector extends Component { return; } - this.props.onChange(reorder(this.props.tooltipProperties, source.index, destination.index)); + this.props.onChange(reorder(this._getTooltipProperties(), source.index, destination.index)); }; _renderProperties() { - if (!this.props.tooltipProperties) { + if (!this.state.selectedFieldProps.length) { return null; } @@ -82,12 +156,12 @@ export class TooltipSelector extends Component { {(provided, snapshot) => ( - this.props.tooltipProperties.map((propertyName, idx) => ( + this.state.selectedFieldProps.map((field, idx) => ( @@ -99,7 +173,7 @@ export class TooltipSelector extends Component { })} > - {this._getPropertyLabel(propertyName)} + {this._getPropertyLabel(field.name)}
{ - return { name: propertyName }; - }) - : []; - return (
@@ -160,11 +227,12 @@ export class TooltipSelector extends Component {
); } } + diff --git a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js index 10488640af99c..9797bce2cf8c9 100644 --- a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js +++ b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js @@ -9,33 +9,59 @@ import { shallow } from 'enzyme'; import { TooltipSelector } from './tooltip_selector'; + +class MockField { + constructor({ name, label, type }) { + this._name = name; + this._label = label; + this._type = type; + } + + getName() { + return this._name; + } + + async getLabel() { + return this._label || 'foobar_label'; + } + + async getDataType() { + return this._type || 'foobar_type'; + } +} + const defaultProps = { - tooltipProperties: ['iso2'], + tooltipFields: [new MockField({ name: 'iso2' })], onChange: (()=>{}), fields: [ - { + new MockField({ name: 'iso2', label: 'ISO 3166-1 alpha-2 code', type: 'string' - }, - { + }), + new MockField({ name: 'iso3', type: 'string' - }, + }) ] }; describe('TooltipSelector', () => { test('should render component', async () => { + const component = shallow( ); - expect(component) - .toMatchSnapshot(); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js index a2308a638542a..ceb0a6ea9f922 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js @@ -14,7 +14,8 @@ import { areLayersLoaded, getRefreshConfig, getMapInitError, - getQueryableUniqueIndexPatternIds + getQueryableUniqueIndexPatternIds, + isToolbarOverlayHidden, } from '../../selectors/map_selectors'; function mapStateToProps(state = {}) { @@ -28,6 +29,7 @@ function mapStateToProps(state = {}) { refreshConfig: getRefreshConfig(state), mapInitError: getMapInitError(state), indexPatternIds: getQueryableUniqueIndexPatternIds(state), + hideToolbarOverlay: isToolbarOverlayHidden(state), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js index 65bd06779cc31..f82d83e2df85d 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js @@ -21,12 +21,11 @@ import uuid from 'uuid/v4'; const RENDER_COMPLETE_EVENT = 'renderComplete'; export class GisMap extends Component { - state = { isInitialLoadRenderTimeoutComplete: false, domId: uuid(), geoFields: [], - } + }; componentDidMount() { this._isMounted = true; @@ -65,9 +64,9 @@ export class GisMap extends Component { if (el) { el.dispatchEvent(new CustomEvent(RENDER_COMPLETE_EVENT, { bubbles: true })); } - } + }; - _loadGeoFields = async (nextIndexPatternIds) => { + _loadGeoFields = async nextIndexPatternIds => { if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { // all ready loaded index pattern ids return; @@ -78,19 +77,22 @@ export class GisMap extends Component { const geoFields = []; try { const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); - indexPatterns.forEach((indexPattern) => { + indexPatterns.forEach(indexPattern => { indexPattern.fields.forEach(field => { - if (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) { + if ( + field.type === ES_GEO_FIELD_TYPE.GEO_POINT || + field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE + ) { geoFields.push({ geoFieldName: field.name, geoFieldType: field.type, indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id + indexPatternId: indexPattern.id, }); } }); }); - } catch(e) { + } catch (e) { // swallow errors. // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis } @@ -100,7 +102,7 @@ export class GisMap extends Component { } this.setState({ geoFields }); - } + }; _setRefreshTimer = () => { const { isPaused, interval } = this.props.refreshConfig; @@ -116,12 +118,9 @@ export class GisMap extends Component { this._clearRefreshTimer(); if (!isPaused && interval > 0) { - this.refreshTimerId = setInterval( - () => { - this.props.triggerRefreshTimer(); - }, - interval - ); + this.refreshTimerId = setInterval(() => { + this.props.triggerRefreshTimer(); + }, interval); } }; @@ -134,16 +133,13 @@ export class GisMap extends Component { // Mapbox does not provide any feedback when rendering is complete. // Temporary solution is just to wait set period of time after data has loaded. _startInitialLoadRenderTimer = () => { - setTimeout( - () => { - if (this._isMounted) { - this.setState({ isInitialLoadRenderTimeoutComplete: true }); - this._onInitialLoadRenderComplete(); - } - }, - 5000 - ); - } + setTimeout(() => { + if (this._isMounted) { + this.setState({ isInitialLoadRenderTimeoutComplete: true }); + this._onInitialLoadRenderComplete(); + } + }, 5000); + }; render() { const { @@ -164,14 +160,12 @@ export class GisMap extends Component {
-

- {mapInitError} -

+

{mapInitError}

); @@ -183,21 +177,15 @@ export class GisMap extends Component { currentPanel = null; } else if (addLayerVisible) { currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ; + currentPanel = ; } else if (layerDetailsVisible) { currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ( - - ); + currentPanel = ; } let exitFullScreenButton; if (isFullScreen) { - exitFullScreenButton = ( - - ); + exitFullScreenButton = ; } return ( - + {!this.props.hideToolbarOverlay && ( + + )} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 20520ad3ff8f1..0086c5067ba12 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -90,6 +90,7 @@ export class FilterEditor extends Component { isOpen={this.state.isPopoverOpen} closePopover={this._close} anchorPosition="leftCenter" + ownFocus >
{ + return { + name: field.getName(), + label: await field.getLabel() + }; + }); + leftFields = await Promise.all(leftFieldPromises); } catch (error) { leftFields = []; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 44629d16e6fb3..01c323d73f19e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,7 +16,6 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; @@ -25,6 +24,9 @@ import { indexPatternService, } from '../../../../kibana_services'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 5552a0f4bd5ee..be21e7d1f9858 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -14,6 +14,9 @@ import { GeometryFilterForm } from '../../../components/geometry_filter_form'; import { UrlOverflowService } from 'ui/error_url_overflow'; import rison from 'rison-node'; +// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. +const META_OVERHEAD = 100; + const urlOverflow = new UrlOverflowService(); export class FeatureGeometryFilterForm extends Component { @@ -70,7 +73,7 @@ export class FeatureGeometryFilterForm extends Component { // Ensure filter will not overflow URL. Filters that contain geometry can be extremely large. // No elasticsearch support for pre-indexed shapes and geo_point spatial queries. - if (window.location.href.length + rison.encode(filter).length > urlOverflow.failLength()) { + if (window.location.href.length + rison.encode(filter).length + META_OVERHEAD > urlOverflow.failLength()) { this.setState({ errorMsg: i18n.translate('xpack.maps.tooltip.geometryFilterForm.filterTooLargeMessage', { defaultMessage: 'Cannot create filter. Filters are added to the URL, and this shape has too many vertices to fit in the URL.' diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 866c099b841a1..7843770327011 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -155,6 +155,7 @@ export class FeatureProperties extends React.Component { } const rows = this.state.properties.map(tooltipProperty => { + const label = tooltipProperty.getPropertyName(); return ( diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index 5265ac15e1790..a38f83a5f1ed0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -20,7 +20,9 @@ import { getLayerList, getMapReady, getGoto, - getScrollZoom + getScrollZoom, + isInteractiveDisabled, + isTooltipControlDisabled, } from '../../../selectors/map_selectors'; import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; @@ -31,7 +33,9 @@ function mapStateToProps(state = {}) { goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), tooltipState: getTooltipState(state), - scrollZoom: getScrollZoom(state) + scrollZoom: getScrollZoom(state), + disableInteractive: isInteractiveDisabled(state), + disableTooltipControl: isTooltipControlDisabled(state) }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 8c272cc6c4244..84d7f8004ad9b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -10,13 +10,10 @@ import { ResizeChecker } from '../../../../../../../../src/plugins/kibana_utils/ import { syncLayerOrderForSingleLayer, removeOrphanedSourcesAndLayers, - addSpritesheetToMap + addSpritesheetToMap, } from './utils'; import { getGlyphUrl, isRetina } from '../../../meta'; -import { - DECIMAL_DEGREES_PRECISION, - ZOOM_PRECISION, -} from '../../../../common/constants'; +import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; import mapboxgl from 'mapbox-gl'; import chrome from 'ui/chrome'; import { spritesheet } from '@elastic/maki'; @@ -26,7 +23,6 @@ import { DrawControl } from './draw_control'; import { TooltipControl } from './tooltip_control'; export class MBMapContainer extends React.Component { - state = { prevLayerList: undefined, hasSyncedLayerList: false, @@ -73,12 +69,15 @@ export class MBMapContainer extends React.Component { _debouncedSync = _.debounce(() => { if (this._isMounted) { if (!this.state.hasSyncedLayerList) { - this.setState({ - hasSyncedLayerList: true - }, () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - }); + this.setState( + { + hasSyncedLayerList: true, + }, + () => { + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); + } + ); } } }, 256); @@ -91,25 +90,24 @@ export class MBMapContainer extends React.Component { zoom: _.round(zoom, ZOOM_PRECISION), center: { lon: _.round(mbCenter.lng, DECIMAL_DEGREES_PRECISION), - lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION) + lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION), }, extent: { minLon: _.round(mbBounds.getWest(), DECIMAL_DEGREES_PRECISION), minLat: _.round(mbBounds.getSouth(), DECIMAL_DEGREES_PRECISION), maxLon: _.round(mbBounds.getEast(), DECIMAL_DEGREES_PRECISION), - maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION) - } + maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION), + }, }; } async _createMbMapInstance() { const initialView = this.props.goto ? this.props.goto.center : null; - return new Promise((resolve) => { - + return new Promise(resolve => { const mbStyle = { version: 8, sources: {}, - layers: [] + layers: [], }; const glyphUrl = getGlyphUrl(); if (glyphUrl) { @@ -121,24 +119,25 @@ export class MBMapContainer extends React.Component { container: this.refs.mapContainer, style: mbStyle, scrollZoom: this.props.scrollZoom, - preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false) + preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false), + interactive: !this.props.disableInteractive, }; if (initialView) { options.zoom = initialView.zoom; options.center = { lng: initialView.lon, - lat: initialView.lat + lat: initialView.lat, }; } const mbMap = new mapboxgl.Map(options); mbMap.dragRotate.disable(); mbMap.touchZoomRotate.disableRotation(); - mbMap.addControl( - new mapboxgl.NavigationControl({ showCompass: false }), 'top-left' - ); + if (!this.props.disableInteractive) { + mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); + } let emptyImage; - mbMap.on('styleimagemissing', (e) => { + mbMap.on('styleimagemissing', e => { if (emptyImage) { mbMap.addImage(e.id, emptyImage); } @@ -146,7 +145,8 @@ export class MBMapContainer extends React.Component { mbMap.on('load', () => { emptyImage = new Image(); // eslint-disable-next-line max-len - emptyImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; + emptyImage.src = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; emptyImage.crossOrigin = 'anonymous'; resolve(mbMap); }); @@ -157,7 +157,7 @@ export class MBMapContainer extends React.Component { let mbMap; try { mbMap = await this._createMbMapInstance(); - } catch(error) { + } catch (error) { this.props.setMapInitError(error.message); return; } @@ -166,14 +166,12 @@ export class MBMapContainer extends React.Component { return; } - this.setState( - { mbMap }, - () => { - this._loadMakiSprites(); - this._initResizerChecker(); - this._registerMapEventListeners(); - this.props.onMapReady(this._getMapState()); - }); + this.setState({ mbMap }, () => { + this._loadMakiSprites(); + this._initResizerChecker(); + this._registerMapEventListeners(); + this.props.onMapReady(this._getMapState()); + }); } _registerMapEventListeners() { @@ -181,14 +179,17 @@ export class MBMapContainer extends React.Component { // moveend is fired while the map extent is still changing in the following scenarios // 1) During opening/closing of layer details panel, the EUI animation results in 8 moveend events // 2) Setting map zoom and center from goto is done in 2 API calls, resulting in 2 moveend events - this.state.mbMap.on('moveend', _.debounce(() => { - this.props.extentChanged(this._getMapState()); - }, 100)); + this.state.mbMap.on( + 'moveend', + _.debounce(() => { + this.props.extentChanged(this._getMapState()); + }, 100) + ); const throttledSetMouseCoordinates = _.throttle(e => { this.props.setMouseCoordinates({ lat: e.lngLat.lat, - lon: e.lngLat.lng + lon: e.lngLat.lng, }); }, 100); this.state.mbMap.on('mousemove', throttledSetMouseCoordinates); @@ -212,11 +213,7 @@ export class MBMapContainer extends React.Component { } _syncMbMapWithMapState = () => { - const { - isMapReady, - goto, - clearGoto, - } = this.props; + const { isMapReady, goto, clearGoto } = this.props; if (!isMapReady || !goto) { return; @@ -227,8 +224,14 @@ export class MBMapContainer extends React.Component { if (goto.bounds) { //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) const lnLatBounds = new mapboxgl.LngLatBounds( - new mapboxgl.LngLat(clamp(goto.bounds.min_lon, -180, 180), clamp(goto.bounds.min_lat, -89, 89)), - new mapboxgl.LngLat(clamp(goto.bounds.max_lon, -180, 180), clamp(goto.bounds.max_lat, -89, 89)), + new mapboxgl.LngLat( + clamp(goto.bounds.min_lon, -180, 180), + clamp(goto.bounds.min_lat, -89, 89) + ), + new mapboxgl.LngLat( + clamp(goto.bounds.max_lon, -180, 180), + clamp(goto.bounds.max_lat, -89, 89) + ) ); //maxZoom ensure we're not zooming in too far on single points or small shapes //the padding is to avoid too tight of a fit around edges @@ -237,7 +240,7 @@ export class MBMapContainer extends React.Component { this.state.mbMap.setZoom(goto.center.zoom); this.state.mbMap.setCenter({ lng: goto.center.lon, - lat: goto.center.lat + lat: goto.center.lat, }); } }; @@ -260,7 +263,6 @@ export class MBMapContainer extends React.Component { const stats = { center: this.state.mbMap.getCenter().toArray(), zoom: this.state.mbMap.getZoom(), - }; this.props.inspectorAdapters.map.setMapState({ stats, @@ -272,20 +274,15 @@ export class MBMapContainer extends React.Component { let drawControl; let tooltipControl; if (this.state.mbMap) { - drawControl = ( - - ); - tooltipControl = ( + drawControl = ; + tooltipControl = !this.props.disableTooltipControl ? ( - ); + ) : null; } return (
{ return LAYER_ID; }, - getLegendDetails: () => { return (
TOC details mock
); }, + renderLegendDetails: () => { return (
TOC details mock
); }, getDisplayName: () => { return 'layer 1'; }, isVisible: () => { return true; }, showAtZoomLevel: () => { return true; }, diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 5643bcba26816..4b04251edd94a 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -11,13 +11,13 @@ import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS, - FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, LAT_INDEX, } from '../common/constants'; import { getEsSpatialRelationLabel } from '../common/i18n_getters'; +import { SPATIAL_FILTER_TYPE } from './kibana_services'; function ensureGeoField(type) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -80,12 +80,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { features.push({ type: 'Feature', geometry: tmpGeometriesAccumulator[j], - properties: { - ...properties, - // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions - // Need to prefix with _index to guarantee uniqueness - [FEATURE_ID_PROPERTY_NAME]: `${properties._index}:${properties._id}:${j}` - } + // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions + // Need to prefix with _index to guarantee uniqueness + id: `${properties._index}:${properties._id}:${j}`, + properties, }); } } @@ -287,6 +285,7 @@ function createGeometryFilterWithMeta({ }) : getEsSpatialRelationLabel(relation); const meta = { + type: SPATIAL_FILTER_TYPE, negate: false, index: indexPatternId, alias: `${geoFieldName} ${relationLabel} ${geometryLabel}` diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index 69f65a5cd11e3..45aa2af15eb9d 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -6,6 +6,11 @@ jest.mock('ui/new_platform'); jest.mock('ui/index_patterns'); +jest.mock('./kibana_services', () => { + return { + SPATIAL_FILTER_TYPE: 'spatial_filter' + }; +}); import { hitsToGeoJson, @@ -69,8 +74,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', }, @@ -134,8 +139,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', myField: 8 @@ -147,8 +152,8 @@ describe('hitsToGeoJson', () => { coordinates: [110, 30], type: 'Point', }, + id: 'index1:doc1:1', properties: { - __kbn__feature_id__: 'index1:doc1:1', _id: 'doc1', _index: 'index1', myField: 8 diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/legacy/plugins/maps/public/embeddable/README.md index ccd7899b1c914..c2952de82c223 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/README.md +++ b/x-pack/legacy/plugins/maps/public/embeddable/README.md @@ -4,6 +4,9 @@ - **isLayerTOCOpen:** (Boolean) Set to false to render map with legend in collapsed state. - **openTOCDetails:** (Array of Strings) Array of layer ids. Add layer id to show layer details on initial render. - **mapCenter:** ({lat, lon, zoom }) Provide mapCenter to customize initial map location. +- **disableInteractive:** (Boolean) Will disable map interactions, panning, zooming in the map. +- **disableTooltipControl:** (Boolean) Will disable tooltip which shows relevant information on hover, like Continent name etc +- **hideToolbarOverlay:** (Boolean) Will disable toolbar, which can be used to navigate to coordinate by entering lat/long and zoom values. ### Creating a Map embeddable from saved object ``` diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index 53931d1953ab3..18c4c78b96974 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -10,7 +10,10 @@ import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; -import { Embeddable, APPLY_FILTER_TRIGGER } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { + Embeddable, + APPLY_FILTER_TRIGGER, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { onlyDisabledFiltersChanged } from '../../../../../../src/plugins/data/public'; import { I18nContext } from 'ui/i18n'; @@ -24,12 +27,11 @@ import { setQuery, setRefreshConfig, disableScrollZoom, + disableInteractive, + disableTooltipControl, + hideToolbarOverlay, } from '../actions/map_actions'; -import { - setReadOnly, - setIsLayerTOCOpen, - setOpenTOCDetails, -} from '../actions/ui_actions'; +import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters, setEventHandlers } from '../reducers/non_serializable_instances'; import { getMapCenter, getMapZoom } from '../selectors/map_selectors'; @@ -47,14 +49,15 @@ export class MapEmbeddable extends Embeddable { editable: config.editable, defaultTitle: config.title, }, - parent); + parent + ); this._renderTooltipContent = renderTooltipContent; this._eventHandlers = eventHandlers; this._layerList = config.layerList; this._store = createMapStore(); - this._subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); + this._subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input)); } getInspectorAdapters() { @@ -62,9 +65,11 @@ export class MapEmbeddable extends Embeddable { } onContainerStateChanged(containerState) { - if (!_.isEqual(containerState.timeRange, this._prevTimeRange) || - !_.isEqual(containerState.query, this._prevQuery) || - !onlyDisabledFiltersChanged(containerState.filters, this._prevFilters)) { + if ( + !_.isEqual(containerState.timeRange, this._prevTimeRange) || + !_.isEqual(containerState.query, this._prevQuery) || + !onlyDisabledFiltersChanged(containerState.filters, this._prevFilters) + ) { this._dispatchSetQuery(containerState); } @@ -77,20 +82,24 @@ export class MapEmbeddable extends Embeddable { this._prevTimeRange = timeRange; this._prevQuery = query; this._prevFilters = filters; - this._store.dispatch(setQuery({ - filters: filters.filter(filter => !filter.meta.disabled), - query, - timeFilters: timeRange, - refresh, - })); + this._store.dispatch( + setQuery({ + filters: filters.filter(filter => !filter.meta.disabled), + query, + timeFilters: timeRange, + refresh, + }) + ); } _dispatchSetRefreshConfig({ refreshConfig }) { this._prevRefreshConfig = refreshConfig; - this._store.dispatch(setRefreshConfig({ - isPaused: refreshConfig.pause, - interval: refreshConfig.value, - })); + this._store.dispatch( + setRefreshConfig({ + isPaused: refreshConfig.pause, + interval: refreshConfig.value, + }) + ); } /** @@ -111,12 +120,26 @@ export class MapEmbeddable extends Embeddable { this._store.dispatch(setOpenTOCDetails(this.input.openTOCDetails)); } + if (_.has(this.input, 'disableInteractive') && this.input.disableInteractive) { + this._store.dispatch(disableInteractive(this.input.disableInteractive)); + } + + if (_.has(this.input, 'disableTooltipControl') && this.input.disableTooltipControl) { + this._store.dispatch(disableTooltipControl(this.input.disableTooltipControl)); + } + + if (_.has(this.input, 'hideToolbarOverlay') && this.input.hideToolbarOverlay) { + this._store.dispatch(hideToolbarOverlay(this.input.hideToolbarOverlay)); + } + if (this.input.mapCenter) { - this._store.dispatch(setGotoWithCenter({ - lat: this.input.mapCenter.lat, - lon: this.input.mapCenter.lon, - zoom: this.input.mapCenter.zoom, - })); + this._store.dispatch( + setGotoWithCenter({ + lat: this.input.mapCenter.lat, + lon: this.input.mapCenter.lon, + zoom: this.input.mapCenter.zoom, + }) + ); } this._store.dispatch(replaceLayerList(this._layerList)); @@ -147,7 +170,7 @@ export class MapEmbeddable extends Embeddable { embeddable: this, filters, }); - } + }; destroy() { super.destroy(); @@ -169,41 +192,41 @@ export class MapEmbeddable extends Embeddable { query: this._prevQuery, timeRange: this._prevTimeRange, filters: this._prevFilters, - refresh: true + refresh: true, }); } _handleStoreChanges() { - const center = getMapCenter(this._store.getState()); const zoom = getMapZoom(this._store.getState()); - const mapCenter = this.input.mapCenter || {}; - if (!mapCenter - || mapCenter.lat !== center.lat - || mapCenter.lon !== center.lon - || mapCenter.zoom !== zoom) { + if ( + !mapCenter || + mapCenter.lat !== center.lat || + mapCenter.lon !== center.lon || + mapCenter.zoom !== zoom + ) { this.updateInput({ mapCenter: { lat: center.lat, lon: center.lon, zoom: zoom, - } + }, }); } const isLayerTOCOpen = getIsLayerTOCOpen(this._store.getState()); if (this.input.isLayerTOCOpen !== isLayerTOCOpen) { this.updateInput({ - isLayerTOCOpen + isLayerTOCOpen, }); } const openTOCDetails = getOpenTOCDetails(this._store.getState()); if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) { this.updateInput({ - openTOCDetails + openTOCDetails, }); } } diff --git a/x-pack/legacy/plugins/maps/public/help_menu_util.js b/x-pack/legacy/plugins/maps/public/help_menu_util.js index c81261554004a..72d51cc180eb3 100644 --- a/x-pack/legacy/plugins/maps/public/help_menu_util.js +++ b/x-pack/legacy/plugins/maps/public/help_menu_util.js @@ -3,15 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { HelpMenu } from './components/help_menu'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; export function addHelpMenuToAppChrome(chrome) { - chrome.helpExtension.set(domElement => { - render(, domElement); - return () => { - unmountComponentAtNode(domElement); - }; + chrome.helpExtension.set({ + appName: 'Maps', + links: [ + { + linkType: 'documentation', + href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, + }, + { + linkType: 'github', + title: '[Maps]', + labels: ['Team:Geo'], + }, + ], }); } diff --git a/x-pack/legacy/plugins/maps/public/index.js b/x-pack/legacy/plugins/maps/public/index.js index 49d8646c6a251..964753f464d95 100644 --- a/x-pack/legacy/plugins/maps/public/index.js +++ b/x-pack/legacy/plugins/maps/public/index.js @@ -10,7 +10,6 @@ import { wrapInI18nContext } from 'ui/i18n'; import { i18n } from '@kbn/i18n'; // import the uiExports that we want to "use" -import 'uiExports/fieldFormats'; import 'uiExports/inspectorViews'; import 'uiExports/search'; import 'uiExports/embeddableFactories'; diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index e2500d7331db6..7169014542710 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../../../../../src/legacy/ui/public/courier'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { esFilters } from '../../../../../src/plugins/data/public'; -export { SearchSource } from 'ui/courier'; +export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; +export { SearchSource } from '../../../../../src/legacy/ui/public/courier'; export const indexPatternService = data.indexPatterns.indexPatterns; export async function fetchSearchSourceAndRecordWithInspector({ diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js new file mode 100644 index 0000000000000..c6da7673ba606 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { TooltipProperty } from '../tooltips/tooltip_property'; + +export class EMSFileField extends AbstractField { + static type = 'EMS_FILE'; + + async getLabel() { + const emsFileLayer = await this._source.getEMSFileLayer(); + const emsFields = emsFileLayer.getFieldsInLanguage(); + // Map EMS field name to language specific label + const emsField = emsFields.find(field => field.name === this.getName()); + return emsField ? emsField.description : this.getName(); + } + + async createTooltipProperty(value) { + const label = await this.getLabel(); + return new TooltipProperty(this.getName(), label, value); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js new file mode 100644 index 0000000000000..eb80169e94eab --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { COUNT_AGG_TYPE } from '../../../common/constants'; +import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; + +export class ESAggMetricField extends AbstractField { + + static type = 'ES_AGG'; + + constructor({ label, source, aggType, esDocField, origin }) { + super({ source, origin }); + this._label = label; + this._aggType = aggType; + this._esDocField = esDocField; + } + + getName() { + return this._source.formatMetricKey(this.getAggType(), this.getESDocFieldName()); + } + + async getLabel() { + return this._label ? await this._label : this._source.formatMetricLabel(this.getAggType(), this.getESDocFieldName()); + } + + getAggType() { + return this._aggType; + } + + isValid() { + return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField; + } + + getESDocFieldName() { + return this._esDocField ? this._esDocField.getName() : ''; + } + + getRequestDescription() { + return this.getAggType() !== COUNT_AGG_TYPE ? `${this.getAggType()} ${this.getESDocFieldName()}` : COUNT_AGG_TYPE; + } + + async createTooltipProperty(value) { + const indexPattern = await this._source.getIndexPattern(); + return new ESAggMetricTooltipProperty( + this.getName(), + await this.getLabel(), + value, + indexPattern, + this + ); + } + + + makeMetricAggConfig() { + const metricAggConfig = { + id: this.getName(), + enabled: true, + type: this.getAggType(), + schema: 'metric', + params: {} + }; + if (this.getAggType() !== COUNT_AGG_TYPE) { + metricAggConfig.params = { field: this.getESDocFieldName() }; + } + return metricAggConfig; + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js new file mode 100644 index 0000000000000..5cc0c9a29ce02 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -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 { AbstractField } from './field'; +import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; + +export class ESDocField extends AbstractField { + + static type = 'ES_DOC'; + + async _getField() { + const indexPattern = await this._source.getIndexPattern(); + return indexPattern.fields.getByName(this._fieldName); + } + + async createTooltipProperty(value) { + const indexPattern = await this._source.getIndexPattern(); + return new ESTooltipProperty(this.getName(), this.getName(), value, indexPattern); + } + + async getDataType() { + const field = await this._getField(); + return field.type; + } + +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js new file mode 100644 index 0000000000000..b53c6991c6ebe --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { FIELD_ORIGIN } from '../../../common/constants'; + +export class AbstractField { + + constructor({ fieldName, source, origin }) { + this._fieldName = fieldName; + this._source = source; + this._origin = origin || FIELD_ORIGIN.SOURCE; + } + + getName() { + return this._fieldName; + } + + getSource() { + return this._source; + } + + isValid() { + return !!this._fieldName; + } + + async getDataType() { + return 'string'; + } + + async getLabel() { + return this._fieldName; + } + + async createTooltipProperty() { + throw new Error('must implement Field#createTooltipProperty'); + } + + getOrigin() { + return this._origin; + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js new file mode 100644 index 0000000000000..248c34173a8c2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { TooltipProperty } from '../tooltips/tooltip_property'; + +export class KibanaRegionField extends AbstractField { + + static type = 'KIBANA_REGION'; + + async getLabel() { + const meta = await this._source.getVectorFileMeta(); + const field = meta.fields.find(f => f.name === this._fieldName); + return field ? field.description : this._fieldName; + } + + async createTooltipProperty(value) { + const label = await this.getLabel(); + return new TooltipProperty(this.getName(), label, value); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index 10e528d19785b..0342975ce3192 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { AbstractLayer } from './layer'; import { VectorLayer } from './vector_layer'; import { HeatmapStyle } from './styles/heatmap/heatmap_style'; @@ -23,17 +22,19 @@ export class HeatmapLayer extends VectorLayer { return heatmapLayerDescriptor; } - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - if (!style) { + constructor({ layerDescriptor, source }) { + super({ layerDescriptor, source }); + if (!layerDescriptor.style) { const defaultStyle = HeatmapStyle.createDescriptor(); this._style = new HeatmapStyle(defaultStyle); + } else { + this._style = new HeatmapStyle(layerDescriptor.style); } } _getPropKeyOfSelectedMetric() { const metricfields = this._source.getMetricFields(); - return metricfields[0].propertyKey; + return metricfields[0].getName(); } _getHeatmapLayerId() { @@ -101,8 +102,8 @@ export class HeatmapLayer extends VectorLayer { return true; } - getLegendDetails() { - const label = _.get(this._source.getMetricFields(), '[0].propertyLabel', ''); - return this._style.getLegendDetails(label); + renderLegendDetails() { + const metricFields = this._source.getMetricFields(); + return this._style.renderLegendDetails(metricFields[0]); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index c1ca1a90c15e6..184fdc0663bd7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -6,13 +6,15 @@ import { ESTermSource } from '../sources/es_term_source'; -import { VectorStyle } from '../styles/vector/vector_style'; +import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; export class InnerJoin { - constructor(joinDescriptor, inspectorAdapters) { + constructor(joinDescriptor, leftSource) { this._descriptor = joinDescriptor; + const inspectorAdapters = leftSource.getInspectorAdapters(); this._rightSource = new ESTermSource(joinDescriptor.right, inspectorAdapters); + this._leftField = this._descriptor.leftField ? leftSource.createField({ fieldName: joinDescriptor.leftField }) : null; } destroy() { @@ -20,21 +22,15 @@ export class InnerJoin { } hasCompleteConfig() { - if (this._descriptor.leftField && this._rightSource) { + if (this._leftField && this._rightSource) { return this._rightSource.hasCompleteConfig(); } return false; } - getRightMetricFields() { - return this._rightSource.getMetricFields(); - } - getJoinFields() { - return this.getRightMetricFields().map(({ propertyKey: name, propertyLabel: label }) => { - return { label, name }; - }); + return this._rightSource.getMetricFields(); } // Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request. @@ -44,18 +40,19 @@ export class InnerJoin { return `join_source_${this._rightSource.getId()}`; } - getLeftFieldName() { - return this._descriptor.leftField; + getLeftField() { + return this._leftField; } - joinPropertiesToFeature(feature, propertiesMap, rightMetricFields) { + joinPropertiesToFeature(feature, propertiesMap) { + const rightMetricFields = this._rightSource.getMetricFields(); // delete feature properties added by previous join for (let j = 0; j < rightMetricFields.length; j++) { - const { propertyKey: metricPropertyKey } = rightMetricFields[j]; + const metricPropertyKey = rightMetricFields[j].getName(); delete feature.properties[metricPropertyKey]; // delete all dynamic properties for metric field - const stylePropertyPrefix = VectorStyle.getComputedFieldNamePrefix(metricPropertyKey); + const stylePropertyPrefix = getComputedFieldNamePrefix(metricPropertyKey); Object.keys(feature.properties).forEach(featurePropertyKey => { if (featurePropertyKey.length >= stylePropertyPrefix.length && featurePropertyKey.substring(0, stylePropertyPrefix.length) === stylePropertyPrefix) { @@ -64,7 +61,7 @@ export class InnerJoin { }); } - const joinKey = feature.properties[this._descriptor.leftField]; + const joinKey = feature.properties[this._leftField.getName()]; const coercedKey = typeof joinKey === 'undefined' || joinKey === null ? null : joinKey.toString(); if (propertiesMap && coercedKey !== null && propertiesMap.has(coercedKey)) { Object.assign(feature.properties, propertiesMap.get(coercedKey)); diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js index c493062723470..02410c64c1c42 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js @@ -23,12 +23,25 @@ const rightSource = { indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', indexPatternTitle: 'kibana_sample_data_logs', term: 'geo.dest', + metrics: [{ type: 'count' }] +}; + +const mockSource = { + getInspectorAdapters() { + }, + createField({ fieldName: name }) { + return { + getName() { + return name; + } + }; + } }; const leftJoin = new InnerJoin({ leftField: 'iso2', right: rightSource -}); +}, mockSource); const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest'; describe('joinPropertiesToFeature', () => { @@ -76,7 +89,7 @@ describe('joinPropertiesToFeature', () => { const leftJoin = new InnerJoin({ leftField: 'zipcode', right: rightSource - }); + }, mockSource); const feature = { properties: { @@ -118,7 +131,7 @@ describe('joinPropertiesToFeature', () => { const leftJoin = new InnerJoin({ leftField: 'code', right: rightSource - }); + }, mockSource); const feature = { properties: { diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 3cde1b4bc0a41..1c2f33df66bf8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -6,8 +6,6 @@ import _ from 'lodash'; import React from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; import { MAX_ZOOM, @@ -19,15 +17,11 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; -const SOURCE_UPDATE_REQUIRED = true; -const NO_SOURCE_UPDATE_REQUIRED = false; - export class AbstractLayer { - constructor({ layerDescriptor, source, style }) { + constructor({ layerDescriptor, source }) { this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); this._source = source; - this._style = style; if (this._descriptor.__dataRequests) { this._dataRequests = this._descriptor.__dataRequests.map(dataRequest => new DataRequest(dataRequest)); } else { @@ -196,7 +190,7 @@ export class AbstractLayer { return false; } - getLegendDetails() { + renderLegendDetails() { return null; } @@ -317,42 +311,7 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } - updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - - const { buffer: previousBuffer } = prevMeta; - const { buffer: newBuffer } = nextMeta; - - if (!previousBuffer) { - return SOURCE_UPDATE_REQUIRED; - } - - if (_.isEqual(previousBuffer, newBuffer)) { - return NO_SOURCE_UPDATE_REQUIRED; - } - const previousBufferGeometry = turf.bboxPolygon([ - previousBuffer.minLon, - previousBuffer.minLat, - previousBuffer.maxLon, - previousBuffer.maxLat - ]); - const newBufferGeometry = turf.bboxPolygon([ - newBuffer.minLon, - newBuffer.minLat, - newBuffer.maxLon, - newBuffer.maxLat - ]); - const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); - - const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); - return doesPreviousBufferContainNewBuffer && !isTrimmed - ? NO_SOURCE_UPDATE_REQUIRED - : SOURCE_UPDATE_REQUIRED; - } getLayerTypeIconName() { throw new Error('should implement Layer#getLayerTypeIconName'); @@ -395,6 +354,10 @@ export class AbstractLayer { return []; } + async getOrdinalFields() { + return []; + } + syncVisibilityWithMb(mbMap, mbLayerId) { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } @@ -404,4 +367,3 @@ export class AbstractLayer { } } - diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.test.js b/x-pack/legacy/plugins/maps/public/layers/layer.test.js deleted file mode 100644 index 98be0855cd4b7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/layer.test.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AbstractLayer } from './layer'; - -describe('layer', () => { - const layer = new AbstractLayer({ layerDescriptor: {} }); - - describe('updateDueToExtent', () => { - - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return false; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when buffers are the same', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when meta has no old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when the new buffer is not contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js index 7c56f4c7f4316..6e931ebe2b95b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js @@ -6,7 +6,7 @@ import React from 'react'; -import { JsonUploadAndParse } from '../../../../../file_upload/public'; +import { start as fileUpload } from '../../../../../file_upload/public/legacy'; export function ClientFileCreateSourceEditor({ previewGeojsonFile, @@ -16,7 +16,7 @@ export function ClientFileCreateSourceEditor({ onIndexReady, }) { return ( - ({ - type: 'Feature', - geometry: feature.geometry, - properties: feature.properties ? { ...feature.properties } : {} - })); return { - data: { - type: 'FeatureCollection', - features: copiedPropsFeatures - }, + data: this._descriptor.__featureCollection, meta: {} }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index 3af1378e8e016..fcd52683b70ff 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,13 +7,13 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMS_FILE, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; -import { TooltipProperty } from '../../tooltips/tooltip_property'; +import { EMSFileField } from '../../fields/ems_file_field'; export class EMSFileSource extends AbstractVectorSource { @@ -45,19 +45,29 @@ export class EMSFileSource extends AbstractVectorSource { constructor(descriptor, inspectorAdapters) { super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters); + this._tooltipFields = this._descriptor.tooltipProperties.map(propertyKey => this.createField({ fieldName: propertyKey })); + } + + createField({ fieldName }) { + return new EMSFileField({ + fieldName, + source: this, + origin: FIELD_ORIGIN.SOURCE + }); } renderSourceSettingsEditor({ onChange }) { return ( ); } - async _getEMSFileLayer() { + async getEMSFileLayer() { const emsClient = getEMSClient(); const emsFileLayers = await emsClient.getFileLayers(); const emsFileLayer = emsFileLayers.find((fileLayer => fileLayer.getId() === this._descriptor.id)); @@ -73,7 +83,7 @@ export class EMSFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); const featureCollection = await AbstractVectorSource.getGeoJson({ format: emsFileLayer.getDefaultFormatType(), featureCollectionPath: 'data', @@ -84,7 +94,7 @@ export class EMSFileSource extends AbstractVectorSource { return field.type === 'id'; }); featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = emsIdField + feature.id = emsIdField ? feature.properties[emsIdField.id] : index; }); @@ -98,7 +108,7 @@ export class EMSFileSource extends AbstractVectorSource { async getImmutableProperties() { let emsLink; try { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); emsLink = emsFileLayer.getEMSHotLink(); } catch(error) { // ignore error if EMS layer id could not be found @@ -121,7 +131,7 @@ export class EMSFileSource extends AbstractVectorSource { async getDisplayName() { try { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); return emsFileLayer.getDisplayName(); } catch (error) { return this._descriptor.id; @@ -129,36 +139,28 @@ export class EMSFileSource extends AbstractVectorSource { } async getAttributions() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); return emsFileLayer.getAttributions(); } async getLeftJoinFields() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); const fields = emsFileLayer.getFieldsInLanguage(); - return fields.map(f => { - return { name: f.name, label: f.description }; - }); + return fields.map(f => this.createField({ fieldName: f.name })); } canFormatFeatureProperties() { - return this._descriptor.tooltipProperties.length; + return this._tooltipFields.length > 0; } async filterAndFormatPropertiesToHtml(properties) { - const emsFileLayer = await this._getEMSFileLayer(); - const emsFields = emsFileLayer.getFieldsInLanguage(); - - return this._descriptor.tooltipProperties.map(propertyName => { - // Map EMS field name to language specific label - const emsField = emsFields.find(field => { - return field.name === propertyName; - }); - const label = emsField ? emsField.description : propertyName; - - return new TooltipProperty(propertyName, label, properties[propertyName]); + const tooltipProperties = this._tooltipFields.map(field => { + const value = properties[field.getName()]; + return field.createTooltipProperty(value); }); + + return Promise.all(tooltipProperties); } async getSupportedShapeTypes() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js index d9f759bdcc2cd..15581f1cfbacb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js @@ -13,7 +13,7 @@ function makeEMSFileSource(tooltipProperties) { const emsFileSource = new EMSFileSource({ tooltipProperties: tooltipProperties }); - emsFileSource._getEMSFileLayer = () => { + emsFileSource.getEMSFileLayer = () => { return { getFieldsInLanguage() { return [{ diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js index ccd7592649e21..f901c8b93e8cd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js @@ -13,7 +13,8 @@ export class UpdateSourceEditor extends Component { static propTypes = { onChange: PropTypes.func.isRequired, - tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired + tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, + source: PropTypes.object }; state = { @@ -36,16 +37,12 @@ export class UpdateSourceEditor extends Component { const emsFiles = await emsClient.getFileLayers(); const emsFile = emsFiles.find((emsFile => emsFile.getId() === this.props.layerId)); const emsFields = emsFile.getFieldsInLanguage(); - fields = emsFields.map(field => { - return { - name: field.name, - label: field.description - }; - }); + fields = emsFields.map(field => this.props.source.createField({ fieldName: field.name })); } catch(e) { //swallow this error. when a matching EMS-config cannot be found, the source already will have thrown errors during the data request. This will propagate to the vector-layer and be displayed in the UX fields = []; } + if (this._isMounted) { this.setState({ fields: fields }); } @@ -56,9 +53,10 @@ export class UpdateSourceEditor extends Component { }; render() { + return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js index d9639144dfc52..fc28f4cf3a900 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js @@ -5,12 +5,10 @@ */ import { AbstractESSource } from './es_source'; -import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; -import { METRIC_TYPE } from '../../../common/constants'; -import _ from 'lodash'; +import { ESAggMetricField } from '../fields/es_agg_field'; +import { ESDocField } from '../fields/es_doc_field'; +import { METRIC_TYPE, COUNT_AGG_TYPE, COUNT_PROP_LABEL, COUNT_PROP_NAME, FIELD_ORIGIN } from '../../../common/constants'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const AGG_DELIMITER = '_of_'; export class AbstractESAggSource extends AbstractESSource { @@ -34,109 +32,106 @@ export class AbstractESAggSource extends AbstractESSource { ] }; - _formatMetricKey(metric) { - const aggType = metric.type; - const fieldName = metric.field; - return aggType !== METRIC_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; + constructor(descriptor, inspectorAdapters) { + super(descriptor, inspectorAdapters); + this._metricFields = this._descriptor.metrics ? this._descriptor.metrics.map(metric => { + const esDocField = metric.field ? new ESDocField({ fieldName: metric.field, source: this }) : null; + return new ESAggMetricField({ + label: metric.label, + esDocField: esDocField, + aggType: metric.type, + source: this, + origin: this.getOriginForField() + }); + }) : []; } - _formatMetricLabel(metric) { - const aggType = metric.type; - const fieldName = metric.field; - return aggType !== METRIC_TYPE.COUNT ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; - } + createField({ fieldName, label }) { - _getValidMetrics() { - const metrics = _.get(this._descriptor, 'metrics', []).filter(({ type, field }) => { - if (type === METRIC_TYPE.COUNT) { - return true; + //if there is a corresponding field with a custom label, use that one. + if (!label) { + const matchField = this._metricFields.find(field => field.getName() === fieldName); + if (matchField) { + label = matchField.getLabel(); } + } - if (field) { - return true; - } - return false; + if (fieldName === COUNT_PROP_NAME) { + return new ESAggMetricField({ + aggType: COUNT_AGG_TYPE, + label: label, + source: this, + origin: this.getOriginForField() + }); + } + //this only works because aggType is a fixed set and does not include the `_of_` string + const [aggType, docField] = fieldName.split(AGG_DELIMITER); + const esDocField = new ESDocField({ fieldName: docField, source: this }); + return new ESAggMetricField({ + label: label, + esDocField, + aggType, + source: this, + origin: this.getOriginForField() + }); + } + + getMetricFieldForName(fieldName) { + return this.getMetricFields().find(metricField => { + return metricField.getName() === fieldName; }); + } + + getOriginForField() { + return FIELD_ORIGIN.SOURCE; + } + + getMetricFields() { + const metrics = this._metricFields.filter(esAggField => esAggField.isValid()); if (metrics.length === 0) { - metrics.push({ type: METRIC_TYPE.COUNT }); + metrics.push(new ESAggMetricField({ + aggType: COUNT_AGG_TYPE, + source: this, + origin: this.getOriginForField() + })); } return metrics; } - getMetricFields() { - return this._getValidMetrics().map(metric => { - const metricKey = this._formatMetricKey(metric); - const metricLabel = metric.label ? metric.label : this._formatMetricLabel(metric); - const metricCopy = { ...metric }; - delete metricCopy.label; - return { - ...metricCopy, - propertyKey: metricKey, - propertyLabel: metricLabel - }; - }); + formatMetricKey(aggType, fieldName) { + return aggType !== COUNT_AGG_TYPE ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; } - async getNumberFields() { - return this.getMetricFields().map(({ propertyKey: name, propertyLabel: label }) => { - return { label, name }; - }); + formatMetricLabel(aggType, fieldName) { + return aggType !== COUNT_AGG_TYPE ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; } - getFieldNames() { - return this.getMetricFields().map(({ propertyKey }) => { - return propertyKey; - }); + createMetricAggConfigs() { + return this.getMetricFields().map(esAggMetric => esAggMetric.makeMetricAggConfig()); } - createMetricAggConfigs() { - return this.getMetricFields().map(metric => { - const metricAggConfig = { - id: metric.propertyKey, - enabled: true, - type: metric.type, - schema: 'metric', - params: {} - }; - if (metric.type !== METRIC_TYPE.COUNT) { - metricAggConfig.params = { field: metric.field }; - } - return metricAggConfig; - }); + + async getNumberFields() { + return this.getMetricFields(); } async filterAndFormatPropertiesToHtmlForMetricFields(properties) { - let indexPattern; - try { - indexPattern = await this._getIndexPattern(); - } catch(error) { - console.warn(`Unable to find Index pattern ${this._descriptor.indexPatternId}, values are not formatted`); - return properties; - } const metricFields = this.getMetricFields(); - const tooltipProperties = []; + const tooltipPropertiesPromises = []; metricFields.forEach((metricField) => { let value; for (const key in properties) { - if (properties.hasOwnProperty(key) && metricField.propertyKey === key) { + if (properties.hasOwnProperty(key) && metricField.getName() === key) { value = properties[key]; break; } } - const tooltipProperty = new ESAggMetricTooltipProperty( - metricField.propertyKey, - metricField.propertyLabel, - value, - indexPattern, - metricField - ); - tooltipProperties.push(tooltipProperty); + const tooltipPromise = metricField.createTooltipProperty(value); + tooltipPropertiesPromises.push(tooltipPromise); }); - return tooltipProperties; - + return await Promise.all(tooltipPropertiesPromises); } - } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js index c83f12ce992ff..d26bfd8bbeacb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js @@ -6,7 +6,7 @@ import { RENDER_AS } from './render_as'; import { getTileBoundingBox } from './geo_tile_utils'; -import { EMPTY_FEATURE_COLLECTION, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMPTY_FEATURE_COLLECTION } from '../../../../common/constants'; export function convertToGeoJson({ table, renderAs }) { @@ -34,9 +34,7 @@ export function convertToGeoJson({ table, renderAs }) { return; } - const properties = { - [FEATURE_ID_PROPERTY_NAME]: gridKey - }; + const properties = {}; metricColumns.forEach(metricColumn => { properties[metricColumn.aggConfig.id] = row[metricColumn.id]; }); @@ -49,6 +47,7 @@ export function convertToGeoJson({ table, renderAs }) { geocentroidColumn, renderAs, }), + id: gridKey, properties }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index 395b6ac5cc431..3d02b075b3b81 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from './render_as'; import { indexPatternService } from '../../../kibana_services'; @@ -22,6 +21,9 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField({ type }) { return [ES_GEO_FIELD_TYPE.GEO_POINT].includes(type); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 53a77dc0c00a8..413f99480a8c2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -20,13 +20,12 @@ import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { GRID_RESOLUTION } from '../../grid_resolution'; -import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID } from '../../../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID, COUNT_PROP_LABEL, COUNT_PROP_NAME } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { AbstractESAggSource } from '../es_agg_source'; +import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const MAX_GEOTILE_LEVEL = 29; const aggSchemas = new Schemas([ @@ -93,7 +92,7 @@ export class ESGeoGridSource extends AbstractESAggSource { async getImmutableProperties() { let indexPatternTitle = this._descriptor.indexPatternId; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; } catch (error) { // ignore error, title will just default to id @@ -124,6 +123,10 @@ export class ESGeoGridSource extends AbstractESAggSource { ]; } + getFieldNames() { + return this.getMetricFields().map((esAggMetricField => esAggMetricField.getName())); + } + isGeoGridPrecisionAware() { return true; } @@ -163,7 +166,7 @@ export class ESGeoGridSource extends AbstractESAggSource { } async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); @@ -225,7 +228,7 @@ export class ESGeoGridSource extends AbstractESAggSource { }); descriptor.style = VectorStyle.createDescriptor({ [vectorStyles.FILL_COLOR]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -236,7 +239,7 @@ export class ESGeoGridSource extends AbstractESAggSource { } }, [vectorStyles.ICON_SIZE]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js index c334776e6c4e8..ae9435dc42c69 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js @@ -6,8 +6,6 @@ import _ from 'lodash'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; - const LAT_INDEX = 0; const LON_INDEX = 1; @@ -47,10 +45,10 @@ export function convertToLines(esResponse) { type: 'LineString', coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest] }, + id: `${dest.join()},${key}`, properties: { - [FEATURE_ID_PROPERTY_NAME]: `${dest.join()},${key}`, ...rest - } + }, }); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js index 9f9789374274a..897ded43be28b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; @@ -20,6 +19,8 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; const GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT]; function filterGeoField({ type }) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 0e224578c5754..01220136b14f3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -14,15 +14,14 @@ import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector/vector_style'; import { vectorStyles } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; -import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW } from '../../../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/agg_types'; import { AbstractESAggSource } from '../es_agg_source'; +import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const MAX_GEOTILE_LEVEL = 29; const aggSchemas = new Schemas([AbstractESAggSource.METRIC_SCHEMA_CONFIG]); @@ -92,7 +91,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getImmutableProperties() { let indexPatternTitle = this._descriptor.indexPatternId; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; } catch (error) { // ignore error, title will just default to id @@ -126,7 +125,7 @@ export class ESPewPewSource extends AbstractESAggSource { createDefaultLayer(options) { const styleDescriptor = VectorStyle.createDescriptor({ [vectorStyles.LINE_COLOR]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -137,7 +136,7 @@ export class ESPewPewSource extends AbstractESAggSource { } }, [vectorStyles.LINE_WIDTH]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -167,7 +166,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const metricAggConfigs = this.createMetricAggConfigs(); const aggConfigs = new AggConfigs(indexPattern, metricAggConfigs, aggSchemas.all); @@ -223,7 +222,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async _getGeoField() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.destGeoField); if (!geoField) { throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap index eb9bba76e4405..1e064fdb0dd7d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap @@ -13,7 +13,7 @@ exports[`should enable sort order select when sort field provided 1`] = ` this.createField({ fieldName: property })); + } + + createField({ fieldName }) { + return new ESDocField({ + fieldName, + source: this + }); } renderSourceSettingsEditor({ onChange }) { return ( { - return { name: field.name, label: field.name }; + return this.createField({ fieldName: field.name }); }); } catch (error) { return []; @@ -100,19 +110,15 @@ export class ESSearchSource extends AbstractESSource { async getDateFields() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); return indexPattern.fields.getByType('date').map(field => { - return { name: field.name, label: field.name }; + return this.createField({ fieldName: field.name }); }); } catch (error) { return []; } } - getMetricFields() { - return []; - } - getFieldNames() { return [this._descriptor.geoField]; } @@ -121,7 +127,7 @@ export class ESSearchSource extends AbstractESSource { let indexPatternTitle = this._descriptor.indexPatternId; let geoFieldType = ''; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; const geoField = await this._getGeoField(); geoFieldType = geoField.type; @@ -171,7 +177,7 @@ export class ESSearchSource extends AbstractESSource { } async _excludeDateFields(fieldNames) { - const dateFieldNames = _.map(await this.getDateFields(), 'name'); + const dateFieldNames = (await this.getDateFields()).map(field => field.getName()); return fieldNames.filter(field => { return !dateFieldNames.includes(field); }); @@ -179,7 +185,7 @@ export class ESSearchSource extends AbstractESSource { // Returns docvalue_fields array for the union of indexPattern's dateFields and request's field names. async _getDateDocvalueFields(searchFields) { - const dateFieldNames = _.map(await this.getDateFields(), 'name'); + const dateFieldNames = (await this.getDateFields()).map(field => field.getName()); return searchFields .filter(fieldName => { return dateFieldNames.includes(fieldName); @@ -198,7 +204,7 @@ export class ESSearchSource extends AbstractESSource { topHitsSize, } = this._descriptor; - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = await this._getGeoField(); const scriptFields = {}; @@ -329,7 +335,7 @@ export class ESSearchSource extends AbstractESSource { ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const unusedMetaFields = indexPattern.metaFields.filter(metaField => { return !['_id', '_index'].includes(metaField); }); @@ -362,11 +368,11 @@ export class ESSearchSource extends AbstractESSource { } canFormatFeatureProperties() { - return this._descriptor.tooltipProperties.length > 0; + return this._tooltipFields.length > 0; } async _loadTooltipProperties(docId, index, indexPattern) { - if (this._descriptor.tooltipProperties.length === 0) { + if (this._tooltipFields.length === 0) { return {}; } @@ -378,7 +384,7 @@ export class ESSearchSource extends AbstractESSource { query: `_id:"${docId}" and _index:${index}` }; searchSource.setField('query', query); - searchSource.setField('fields', this._descriptor.tooltipProperties); + searchSource.setField('fields', this._getTooltipPropertyNames()); const resp = await searchSource.fetch(); @@ -394,7 +400,7 @@ export class ESSearchSource extends AbstractESSource { const properties = indexPattern.flattenHit(hit); indexPattern.metaFields.forEach(metaField => { - if (!this._descriptor.tooltipProperties.includes(metaField)) { + if (!this._getTooltipPropertyNames().includes(metaField)) { delete properties[metaField]; } }); @@ -402,12 +408,13 @@ export class ESSearchSource extends AbstractESSource { } async filterAndFormatPropertiesToHtml(properties) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const propertyValues = await this._loadTooltipProperties(properties._id, properties._index, indexPattern); - - return this._descriptor.tooltipProperties.map(propertyName => { - return new ESTooltipProperty(propertyName, propertyName, propertyValues[propertyName], indexPattern); + const tooltipProperties = this._tooltipFields.map(field => { + const value = propertyValues[field.getName()]; + return field.createTooltipProperty(value); }); + return Promise.all(tooltipProperties); } isFilterByMapBounds() { @@ -415,12 +422,9 @@ export class ESSearchSource extends AbstractESSource { } async getLeftJoinFields() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); // Left fields are retrieved from _source. - return getSourceFields(indexPattern.fields) - .map(field => { - return { name: field.name, label: field.name }; - }); + return getSourceFields(indexPattern.fields).map(field => this.createField({ fieldName: field.name })); } async getSupportedShapeTypes() { @@ -507,7 +511,6 @@ export class ESSearchSource extends AbstractESSource { async getPreIndexedShape(properties) { const geoField = await this._getGeoField(); - return { index: properties._index, // Can not use index pattern title because it may reference many indices id: properties._id, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 760e087f2e6f6..1b4e999c29d0a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -22,22 +22,24 @@ import { i18n } from '@kbn/i18n'; import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; import { SORT_ORDER } from '../../../../common/constants'; +import { ESDocField } from '../../fields/es_doc_field'; export class UpdateSourceEditor extends Component { static propTypes = { indexPatternId: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, filterByMapBounds: PropTypes.bool.isRequired, - tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired, + tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, useTopHits: PropTypes.bool.isRequired, topHitsSplitField: PropTypes.string, topHitsSize: PropTypes.number.isRequired, + source: PropTypes.object }; state = { - tooltipFields: null, + sourceFields: null, termFields: null, sortFields: null, }; @@ -73,10 +75,19 @@ export class UpdateSourceEditor extends Component { return; } + //todo move this all to the source + const rawTooltipFields = getSourceFields(indexPattern.fields); + const sourceFields = rawTooltipFields.map(field => { + return new ESDocField({ + fieldName: field.name, + source: this.props.source + }); + }); + this.setState({ - tooltipFields: getSourceFields(indexPattern.fields), - termFields: getTermsFields(indexPattern.fields), - sortFields: indexPattern.fields.filter(field => field.sortable), + sourceFields: sourceFields, + termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields + sortFields: indexPattern.fields.filter(field => field.sortable), //todo change sort fields to use fields }); } _onTooltipPropertiesChange = propertyNames => { @@ -173,9 +184,9 @@ export class UpdateSourceEditor extends Component { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js index 9a3a74e0ed680..5a1b83589a1ee 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js @@ -15,7 +15,7 @@ const defaultProps = { indexPatternId: 'indexPattern1', onChange: () => {}, filterByMapBounds: true, - tooltipProperties: [], + tooltipFields: [], sortOrder: 'DESC', useTopHits: false, topHitsSplitField: 'trackId', diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 010b9360d6501..c2f4f7e755288 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -69,6 +69,10 @@ export class AbstractESSource extends AbstractVectorSource { return clonedDescriptor; } + getMetricFields() { + return []; + } + async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -95,7 +99,7 @@ export class AbstractESSource extends AbstractVectorSource { } async _makeSearchSource(searchFilters, limit, initialSearchContext) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const isTimeAware = await this.isTimeAware(); const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true); const globalFilters = applyGlobalQuery ? searchFilters.filters : []; @@ -130,7 +134,7 @@ export class AbstractESSource extends AbstractVectorSource { const searchSource = await this._makeSearchSource({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0); const geoField = await this._getGeoField(); - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoBoundsAgg = [{ type: 'geo_bounds', @@ -171,7 +175,7 @@ export class AbstractESSource extends AbstractVectorSource { async isTimeAware() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const timeField = indexPattern.timeFieldName; return !!timeField; } catch (error) { @@ -179,7 +183,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _getIndexPattern() { + async getIndexPattern() { if (this.indexPattern) { return this.indexPattern; } @@ -208,7 +212,7 @@ export class AbstractESSource extends AbstractVectorSource { async _getGeoField() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.geoField); if (!geoField) { throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', { @@ -221,7 +225,7 @@ export class AbstractESSource extends AbstractVectorSource { async getDisplayName() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); return indexPattern.title; } catch (error) { // Unable to load index pattern, just return id as display name @@ -238,25 +242,27 @@ export class AbstractESSource extends AbstractVectorSource { } async getFieldFormatter(fieldName) { - const metricField = this.getMetricFields().find(({ propertyKey }) => { - return propertyKey === fieldName; - }); + + const metricField = this.getMetricFields().find(field => field.getName() === fieldName); // Do not use field formatters for counting metrics - if (metricField && metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT) { + if (metricField && (metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT)) { + return null; + } + + // fieldName could be an aggregation so it needs to be unpacked to expose raw field. + const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; + if (!realFieldName) { return null; } let indexPattern; try { - indexPattern = await this._getIndexPattern(); + indexPattern = await this.getIndexPattern(); } catch(error) { return null; } - const realFieldName = metricField - ? metricField.field - : fieldName; const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName); if (!fieldFromIndexPattern) { return null; @@ -264,4 +270,5 @@ export class AbstractESSource extends AbstractVectorSource { return fieldFromIndexPattern.format.getConverterFor('text'); } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index 7d1ccf7373cf6..afc402fa81bcb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -9,12 +9,15 @@ import _ from 'lodash'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/agg_types'; import { i18n } from '@kbn/i18n'; -import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; -import { ES_SIZE_LIMIT, METRIC_TYPE } from '../../../common/constants'; +import { ES_SIZE_LIMIT, FIELD_ORIGIN, METRIC_TYPE } from '../../../common/constants'; +import { ESDocField } from '../fields/es_doc_field'; import { AbstractESAggSource } from './es_agg_source'; const TERMS_AGG_NAME = 'join'; +const FIELD_NAME_PREFIX = '__kbnjoin__'; +const GROUP_BY_DELIMITER = '_groupby_'; + const aggSchemas = new Schemas([ AbstractESAggSource.METRIC_SCHEMA_CONFIG, { @@ -48,6 +51,10 @@ export class ESTermSource extends AbstractESAggSource { static type = 'ES_TERM_SOURCE'; + constructor(descriptor, inspectorAdapters) { + super(descriptor, inspectorAdapters); + this._termField = new ESDocField({ fieldName: descriptor.term, source: this, origin: this.getOriginForField() }); + } static renderEditor({}) { //no need to localize. this editor is never rendered. @@ -62,22 +69,26 @@ export class ESTermSource extends AbstractESAggSource { return [this._descriptor.indexPatternId]; } - getTerm() { - return this._descriptor.term; + getTermField() { + return this._termField; + } + + getOriginForField() { + return FIELD_ORIGIN.JOIN; } getWhereQuery() { return this._descriptor.whereQuery; } - _formatMetricKey(metric) { - const metricKey = metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : metric.type; - return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; + formatMetricKey(aggType, fieldName) { + const metricKey = aggType !== METRIC_TYPE.COUNT ? `${aggType}_of_${fieldName}` : aggType; + return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; } - _formatMetricLabel(metric) { - const metricLabel = metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count'; - return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`; + formatMetricLabel(type, fieldName) { + const metricLabel = type !== METRIC_TYPE.COUNT ? `${type} ${fieldName}` : 'count'; + return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._termField.getName()}`; } async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { @@ -86,13 +97,13 @@ export class ESTermSource extends AbstractESAggSource { return []; } - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const searchSource = await this._makeSearchSource(searchFilters, 0); const configStates = this._makeAggConfigs(); const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const requestName = `${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; + const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); @@ -117,15 +128,13 @@ export class ESTermSource extends AbstractESAggSource { } _getRequestDescription(leftSourceName, leftFieldName) { - const metrics = this._getValidMetrics().map(metric => { - return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count'; - }); + const metrics = this.getMetricFields().map(esAggMetric => esAggMetric.getRequestDescription()); const joinStatement = []; joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinLeftDescription', { defaultMessage: `Join {leftSourceName}:{leftFieldName} with`, values: { leftSourceName, leftFieldName } })); - joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._descriptor.term}`); + joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._termField.getName()}`); joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinMetricsDescription', { defaultMessage: `for metrics {metrics}`, values: { metrics: metrics.join(',') } @@ -148,7 +157,7 @@ export class ESTermSource extends AbstractESAggSource { type: 'terms', schema: 'segment', params: { - field: this._descriptor.term, + field: this._termField.getName(), size: ES_SIZE_LIMIT } } @@ -164,15 +173,7 @@ export class ESTermSource extends AbstractESAggSource { return await this.filterAndFormatPropertiesToHtmlForMetricFields(properties); } - async createESTooltipProperty(propertyName, rawValue) { - try { - const indexPattern = await this._getIndexPattern(); - if (!indexPattern) { - return null; - } - return new ESTooltipProperty(propertyName, propertyName, rawValue, indexPattern); - } catch (e) { - return null; - } + getFieldNames() { + return this.getMetricFields().map(esAggMetricField => esAggMetricField.getName()); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js index ea11c7e367e5b..7f6415fcfae85 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js @@ -36,21 +36,21 @@ const metricExamples = [ describe('getMetricFields', () => { - it('should add default "count" metric when no metrics are provided', () => { + it('should add default "count" metric when no metrics are provided', async () => { const source = new ESTermSource({ indexPatternTitle: indexPatternTitle, term: termFieldName, }); const metrics = source.getMetricFields(); expect(metrics.length).toBe(1); - expect(metrics[0]).toEqual({ - type: 'count', - propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField', - propertyLabel: 'count of myIndex:myTermField', - }); + + expect(metrics[0].getAggType()).toEqual('count'); + expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(await metrics[0].getLabel()).toEqual('count of myIndex:myTermField'); + }); - it('should remove incomplete metric configurations', () => { + it('should remove incomplete metric configurations', async () => { const source = new ESTermSource({ indexPatternTitle: indexPatternTitle, term: termFieldName, @@ -58,17 +58,16 @@ describe('getMetricFields', () => { }); const metrics = source.getMetricFields(); expect(metrics.length).toBe(2); - expect(metrics[0]).toEqual({ - type: 'sum', - field: sumFieldName, - propertyKey: '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField', - propertyLabel: 'my custom label', - }); - expect(metrics[1]).toEqual({ - type: 'count', - propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField', - propertyLabel: 'count of myIndex:myTermField', - }); + + expect(metrics[0].getAggType()).toEqual('sum'); + expect(metrics[0].getESDocFieldName()).toEqual(sumFieldName); + expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField'); + expect(await metrics[0].getLabel()).toEqual('my custom label'); + + expect(metrics[1].getAggType()).toEqual('count'); + expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(await metrics[1].getLabel()).toEqual('count of myIndex:myTermField'); + }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index c75c5600aaf92..e29887edcf7d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,8 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { FIELD_ORIGIN } from '../../../../common/constants'; +import { KibanaRegionField } from '../../fields/kibana_region_field'; export class KibanaRegionmapSource extends AbstractVectorSource { @@ -45,11 +46,20 @@ export class KibanaRegionmapSource extends AbstractVectorSource { ); }; + createField({ fieldName }) { + return new KibanaRegionField({ + fieldName, + source: this, + origin: FIELD_ORIGIN.SOURCE + }); + } + async getImmutableProperties() { return [ { label: getDataSourceLabel(), - value: KibanaRegionmapSource.title }, + value: KibanaRegionmapSource.title + }, { label: i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', { defaultMessage: 'Vector layer' @@ -59,7 +69,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource { ]; } - async _getVectorFileMeta() { + async getVectorFileMeta() { const regionList = getKibanaRegionList(); const meta = regionList.find(source => source.name === this._descriptor.name); if (!meta) { @@ -75,25 +85,20 @@ export class KibanaRegionmapSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const vectorFileMeta = await this._getVectorFileMeta(); + const vectorFileMeta = await this.getVectorFileMeta(); const featureCollection = await AbstractVectorSource.getGeoJson({ format: vectorFileMeta.format.type, featureCollectionPath: vectorFileMeta.meta.feature_collection_path, fetchUrl: vectorFileMeta.url }); - featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = index; - }); return { data: featureCollection }; } async getLeftJoinFields() { - const vectorFileMeta = await this._getVectorFileMeta(); - return vectorFileMeta.fields.map(f => { - return { name: f.name, label: f.description }; - }); + const vectorFileMeta = await this.getVectorFileMeta(); + return vectorFileMeta.fields.map(f => this.createField({ fieldName: f.name })); } async getDisplayName() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index 8f67d7618049b..e255e2478a37d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -48,6 +48,10 @@ export class AbstractVectorSource extends AbstractSource { })); } + createField() { + throw new Error(`Should implemement ${this.constructor.type} ${this}`); + } + _createDefaultLayerDescriptor(options, mapColors) { return VectorLayer.createDescriptor( { @@ -57,6 +61,10 @@ export class AbstractVectorSource extends AbstractSource { mapColors); } + _getTooltipPropertyNames() { + return this._tooltipFields.map(field => field.getName()); + } + createDefaultLayer(options, mapColors) { const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); const style = new VectorStyle(layerDescriptor.style, this); @@ -131,4 +139,5 @@ export class AbstractVectorSource extends AbstractSource { getSourceTooltipContent(/* sourceDataRequest */) { return { tooltipContent: null, areResultsTrimmed: false }; } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js index 06709ba0ebf21..bb10b7686ae3b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js @@ -15,26 +15,54 @@ import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants'; -export function HeatmapLegend({ colorRampName, label }) { - const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME - ? - : ; - - return ( - - ); +export class HeatmapLegend extends React.Component { + + constructor() { + super(); + this.state = { label: '' }; + } + + componentDidUpdate() { + this._loadLabel(); + } + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); + } + componentWillUnmount() { + this._isMounted = false; + } + + async _loadLabel() { + const label = await this.props.field.getLabel(); + if (this._isMounted && this.state.label !== label) { + this.setState({ label }); + } + } + + render() { + const colorRampName = this.props.colorRampName; + const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME + ? + : ; + + return ( + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index e537da8a3e2e4..e4982c86b53bb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -50,11 +50,11 @@ export class HeatmapStyle extends AbstractStyle { ); } - getLegendDetails(label) { + renderLegendDetails(field) { return ( ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 4d091389a360e..35c7066b7fd0f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -5,71 +5,13 @@ */ import _ from 'lodash'; -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { styleOptionShapes, rangeShape } from '../style_option_shapes'; -import { VectorStyle } from '../../vector_style'; -import { ColorGradient } from '../../../components/color_gradient'; -import { CircleIcon } from './circle_icon'; +import { rangeShape } from '../style_option_shapes'; import { getVectorStyleLabel } from '../get_vector_style_label'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { StyleLegendRow } from '../../../components/style_legend_row'; -function getLineWidthIcons() { - const defaultStyle = { - stroke: 'grey', - fill: 'none', - width: '12px', - }; - return [ - , - , - , - ]; -} - -function getSymbolSizeIcons() { - const defaultStyle = { - stroke: 'grey', - strokeWidth: 'none', - fill: 'grey', - }; - return [ - , - , - , - ]; -} - -function renderHeaderWithIcons(icons) { - return ( - - { - icons.map((icon, index) => { - const isLast = index === icons.length - 1; - let spacer; - if (!isLast) { - spacer = ( - - - - ); - } - return ( - - - {icon} - - {spacer} - - ); - }) - } - - ); -} - const EMPTY_VALUE = ''; export class StylePropertyLegendRow extends Component { @@ -97,19 +39,25 @@ export class StylePropertyLegendRow extends Component { } async _loadFieldFormatter() { - this._fieldValueFormatter = await this.props.getFieldFormatter(this.props.options.field); + if (this.props.style.isDynamic() && this.props.style.isComplete() && this.props.style.getField().getSource()) { + const field = this.props.style.getField(); + const source = field.getSource(); + this._fieldValueFormatter = await source.getFieldFormatter(field.getName()); + } else { + this._fieldValueFormatter = null; + } if (this._isMounted) { this.setState({ hasLoadedFieldFormatter: true }); } } _loadLabel = async () => { - if (this._isStatic()) { + if (this._excludeFromHeader()) { return; } // have to load label and then check for changes since field name stays constant while label may change - const label = await this.props.getFieldLabel(this.props.options.field.name); + const label = await this.props.style.getField().getLabel(); if (this._prevLabel === label) { return; } @@ -120,9 +68,8 @@ export class StylePropertyLegendRow extends Component { } } - _isStatic() { - return this.props.type === VectorStyle.STYLE_TYPE.STATIC || - !this.props.options.field || !this.props.options.field.name; + _excludeFromHeader() { + return !this.props.style.isDynamic() || !this.props.style.isComplete() || !this.props.style.getField().getName(); } _formatValue = value => { @@ -134,26 +81,19 @@ export class StylePropertyLegendRow extends Component { } render() { - const { name, options, range } = this.props; - if (this._isStatic()) { - return null; - } - let header; - if (options.color) { - header = ; - } else if (name === 'lineWidth') { - header = renderHeaderWithIcons(getLineWidthIcons()); - } else if (name === 'iconSize') { - header = renderHeaderWithIcons(getSymbolSizeIcons()); + const { range, style } = this.props; + if (this._excludeFromHeader()) { + return null; } + const header = style.renderHeader(); return ( ); @@ -161,10 +101,6 @@ export class StylePropertyLegendRow extends Component { } StylePropertyLegendRow.propTypes = { - name: PropTypes.string.isRequired, - type: PropTypes.string, - options: PropTypes.oneOfType(styleOptionShapes).isRequired, range: rangeShape, - getFieldLabel: PropTypes.func.isRequired, - getFieldFormatter: PropTypes.func.isRequired, + style: PropTypes.object }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js index 60baaff158377..e339cad6af973 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js @@ -7,34 +7,26 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { styleOptionShapes, rangeShape } from '../style_option_shapes'; +import { rangeShape } from '../style_option_shapes'; import { StylePropertyLegendRow } from './style_property_legend_row'; -export function VectorStyleLegend({ getFieldLabel, getFieldFormatter, styleProperties }) { +export function VectorStyleLegend({ styleProperties }) { return styleProperties.map(styleProperty => { return ( ); }); } const stylePropertyShape = PropTypes.shape({ - name: PropTypes.string.isRequired, - type: PropTypes.string, - options: PropTypes.oneOfType(styleOptionShapes).isRequired, range: rangeShape, + style: PropTypes.object }); VectorStyleLegend.propTypes = { - styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired, - getFieldLabel: PropTypes.func.isRequired, - getFieldFormatter: PropTypes.func.isRequired, + styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js index d595dccaea425..a2edc8cb4f686 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js @@ -36,15 +36,6 @@ export const dynamicSizeShape = PropTypes.shape({ field: fieldShape, }); -export const styleOptionShapes = [ - staticColorShape, - dynamicColorShape, - staticOrientationShape, - dynamicOrientationShape, - staticSizeShape, - dynamicSizeShape -]; - export const rangeShape = PropTypes.shape({ min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index f624f4e661a14..c8e4150fd2c26 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -52,20 +52,27 @@ export class VectorStyleEditor extends Component { async _loadOrdinalFields() { + const getFieldMeta = async (field) => { + return { + label: await field.getLabel(), + name: field.getName(), + origin: field.getOrigin() + }; + }; const dateFields = await this.props.layer.getDateFields(); - if (!this._isMounted) { - return; - } - if (!_.isEqual(dateFields, this.state.dateFields)) { - this.setState({ dateFields }); + const dateFieldPromises = dateFields.map(getFieldMeta); + const dateFieldsArray = await Promise.all(dateFieldPromises); + + if (this._isMounted && !_.isEqual(dateFieldsArray, this.state.dateFields)) { + this.setState({ dateFields: dateFieldsArray }); } const numberFields = await this.props.layer.getNumberFields(); - if (!this._isMounted) { - return; - } - if (!_.isEqual(numberFields, this.state.numberFields)) { - this.setState({ numberFields }); + const numberFieldPromises = numberFields.map(getFieldMeta); + + const numberFieldsArray = await Promise.all(numberFieldPromises); + if (this._isMounted && !_.isEqual(numberFieldsArray, this.state.numberFields)) { + this.setState({ numberFields: numberFieldsArray }); } } 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 5c2122dfc4566..4b4b853c274cb 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 @@ -9,11 +9,12 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import _ from 'lodash'; import { getComputedFieldName } from '../style_util'; import { getColorRampStops } from '../../color_utils'; +import { ColorGradient } from '../../components/color_gradient'; +import React from 'react'; export class DynamicColorProperty extends DynamicStyleProperty { - syncCircleColorWithMb(mbLayerId, mbMap, alpha) { const color = this._getMbColor(); mbMap.setPaintProperty(mbLayerId, 'circle-color', color); @@ -48,6 +49,18 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } + isCustomColorRamp() { + return !!this._options.customColorRamp; + } + + supportsFeatureState() { + return true; + } + + isScaled() { + return !this.isCustomColorRamp(); + } + _getMbColor() { const isDynamicConfigComplete = _.has(this._options, 'field.name') && _.has(this._options, 'color'); if (!isDynamicConfigComplete) { @@ -98,6 +111,14 @@ export class DynamicColorProperty extends DynamicStyleProperty { return getColorRampStops(this._options.color); } + renderHeader() { + if (this._options.color) { + return (); + } else { + return null; + } + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index 2881ee422048b..fb4ffd8cce4b4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -22,6 +22,14 @@ export class DynamicOrientationProperty extends DynamicStyleProperty { } } + supportsFeatureState() { + return false; + } + + isScaled() { + return false; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index 2758a440f57c5..bd011b27d81c8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -10,6 +10,34 @@ import { getComputedFieldName } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils'; import { vectorStyles } from '../vector_style_defaults'; import _ from 'lodash'; +import { CircleIcon } from '../components/legend/circle_icon'; +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; + +function getLineWidthIcons() { + const defaultStyle = { + stroke: 'grey', + fill: 'none', + width: '12px', + }; + return [ + , + , + , + ]; +} + +function getSymbolSizeIcons() { + const defaultStyle = { + stroke: 'grey', + fill: 'grey', + }; + return [ + , + , + , + ]; +} export class DynamicSizeProperty extends DynamicStyleProperty { @@ -79,6 +107,43 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _isSizeDynamicConfigComplete() { - return this._options.field && this._options.field.name && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize'); + return this._field && this._field.isValid() && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize'); + } + + renderHeader() { + let icons; + if (this.getStyleName() === vectorStyles.LINE_WIDTH) { + icons = getLineWidthIcons(); + } else if (this.getStyleName() === vectorStyles.ICON_SIZE) { + icons = getSymbolSizeIcons(); + } else { + return null; + } + + return ( + + { + icons.map((icon, index) => { + const isLast = index === icons.length - 1; + let spacer; + if (!isLast) { + spacer = ( + + + + ); + } + return ( + + + {icon} + + {spacer} + + ); + }) + } + + ); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 8200ede3e3523..e87bcc12c99be 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -6,7 +6,37 @@ import { AbstractStyleProperty } from './style_property'; +import { STYLE_TYPE } from '../../../../../common/constants'; export class DynamicStyleProperty extends AbstractStyleProperty { - static type = 'DYNAMIC'; + static type = STYLE_TYPE.DYNAMIC; + + constructor(options, styleName, field) { + super(options, styleName); + this._field = field; + } + + getField() { + return this._field; + } + + isDynamic() { + return true; + } + + isComplete() { + return !!this._field; + } + + getFieldOrigin() { + return this._field.getOrigin(); + } + + supportsFeatureState() { + return true; + } + + isScaled() { + return true; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js index 448efc06899e5..6c53e00f8bd20 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js @@ -6,8 +6,8 @@ import { AbstractStyleProperty } from './style_property'; +import { STYLE_TYPE } from '../../../../../common/constants'; export class StaticStyleProperty extends AbstractStyleProperty { - static type = 'STATIC'; - + static type = STYLE_TYPE.STATIC; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js index 7e9e27f83722d..9d182eac9fa8a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js @@ -10,4 +10,30 @@ export class AbstractStyleProperty { this._options = options; this._styleName = styleName; } + + isDynamic() { + return false; + } + + /** + * Is the style fully defined and usable? (e.g. for rendering, in legend UX, ...) + * Why? during editing, partially-completed descriptors may be added to the layer-descriptor + * e.g. dynamic-fields can have an incomplete state when the field is not yet selected from the drop-down + * @returns {boolean} + */ + isComplete() { + return true; + } + + getStyleName() { + return this._styleName; + } + + getOptions() { + return this._options || {}; + } + + renderHeader() { + return null; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 70ebba7e8d177..2e2f10cc74935 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -6,17 +6,16 @@ import _ from 'lodash'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, vectorStyles } from './vector_style_defaults'; import { AbstractStyle } from '../abstract_style'; -import { SOURCE_DATA_ID_ORIGIN, GEO_JSON_TYPE } from '../../../../common/constants'; +import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants'; import { getMakiSymbolAnchor } from './symbol_utils'; -import { getComputedFieldName, getComputedFieldNamePrefix } from './style_util'; +import { getComputedFieldName } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -33,14 +32,22 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; export class VectorStyle extends AbstractStyle { static type = 'VECTOR'; - static STYLE_TYPE = { 'DYNAMIC': DynamicStyleProperty.type, 'STATIC': StaticStyleProperty.type }; + static STYLE_TYPE = STYLE_TYPE; + static createDescriptor(properties = {}) { + return { + type: VectorStyle.type, + properties: { ...getDefaultProperties(), ...properties } + }; + } - static getComputedFieldName = getComputedFieldName; - static getComputedFieldNamePrefix = getComputedFieldNamePrefix; + static createDefaultStyleProperties(mapColors) { + return getDefaultProperties(mapColors); + } - constructor(descriptor = {}, source) { + constructor(descriptor = {}, source, layer) { super(); this._source = source; + this._layer = layer; this._descriptor = { ...descriptor, ...VectorStyle.createDescriptor(descriptor.properties), @@ -52,29 +59,21 @@ export class VectorStyle extends AbstractStyle { this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE); // eslint-disable-next-line max-len this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION); - } - static createDescriptor(properties = {}) { - return { - type: VectorStyle.type, - properties: { ...getDefaultProperties(), ...properties } - }; } - static createDefaultStyleProperties(mapColors) { - return getDefaultProperties(mapColors); + _getAllStyleProperties() { + return [ + this._lineColorStyleProperty, + this._fillColorStyleProperty, + this._lineWidthStyleProperty, + this._iconSizeStyleProperty, + this._iconOrientationProperty + ]; } - static getDisplayName() { - return i18n.translate('xpack.maps.style.vector.displayNameLabel', { - defaultMessage: 'Vector style' - }); - } - - static description = ''; - renderEditor({ layer, onStyleDescriptorChange }) { - const styleProperties = { ...this.getProperties() }; + const styleProperties = { ...this.getRawProperties() }; const handlePropertyChange = (propertyName, settings) => { styleProperties[propertyName] = settings;//override single property, but preserve the rest const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties); @@ -104,39 +103,45 @@ export class VectorStyle extends AbstractStyle { * can then use to update store state via dispatch. */ getDescriptorWithMissingStylePropsRemoved(nextOrdinalFields) { - const originalProperties = this.getProperties(); + + const originalProperties = this.getRawProperties(); const updatedProperties = {}; - Object.keys(originalProperties).forEach(propertyName => { - if (!this._isPropertyDynamic(propertyName)) { - return; - } - const fieldName = _.get(originalProperties[propertyName], 'options.field.name'); + const dynamicProperties = Object.keys(originalProperties).filter(key => { + const { type, options } = originalProperties[key] || {}; + return type === STYLE_TYPE.DYNAMIC && options.field && options.field.name; + }); + + dynamicProperties.forEach(key => { + + const dynamicProperty = originalProperties[key]; + const fieldName = dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name; if (!fieldName) { return; } - const matchingOrdinalField = nextOrdinalFields.find(oridinalField => { - return fieldName === oridinalField.name; + const matchingOrdinalField = nextOrdinalFields.find(ordinalField => { + return fieldName === ordinalField.getName(); }); if (matchingOrdinalField) { return; } - updatedProperties[propertyName] = { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + updatedProperties[key] = { + type: DynamicStyleProperty.type, options: { - ...originalProperties[propertyName].options + ...originalProperties[key].options } }; - delete updatedProperties[propertyName].options.field; + delete updatedProperties[key].options.field; + }); if (Object.keys(updatedProperties).length === 0) { return { hasChanges: false, - nextStyleDescriptor: { ...this._descriptor }, + nextStyleDescriptor: { ...this._descriptor } }; } @@ -156,9 +161,9 @@ export class VectorStyle extends AbstractStyle { } const scaledFields = this.getDynamicPropertiesArray() - .map(({ options }) => { + .map(styleProperty => { return { - name: options.field.name, + name: styleProperty.getField().getName(), min: Infinity, max: -Infinity }; @@ -219,45 +224,22 @@ export class VectorStyle extends AbstractStyle { } getSourceFieldNames() { - const properties = this.getProperties(); const fieldNames = []; - Object.keys(properties).forEach(propertyName => { - if (!this._isPropertyDynamic(propertyName)) { - return; - } - - const field = _.get(properties[propertyName], 'options.field', {}); - if (field.origin === SOURCE_DATA_ID_ORIGIN && field.name) { - fieldNames.push(field.name); + this.getDynamicPropertiesArray().forEach(styleProperty => { + if (styleProperty.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + fieldNames.push(styleProperty.getField().getName()); } }); - return fieldNames; } - getProperties() { + getRawProperties() { return this._descriptor.properties || {}; } getDynamicPropertiesArray() { - const styles = this.getProperties(); - return Object.keys(styles) - .map(styleName => { - const { type, options } = styles[styleName]; - return { - styleName, - type, - options - }; - }) - .filter(({ styleName }) => { - return this._isPropertyDynamic(styleName); - }); - } - - _isPropertyDynamic(propertyName) { - const { type, options } = _.get(this._descriptor, ['properties', propertyName], {}); - return type === VectorStyle.STYLE_TYPE.DYNAMIC && options.field && options.field.name; + const styleProperties = this._getAllStyleProperties(); + return styleProperties.filter(styleProperty => (styleProperty.isDynamic() && styleProperty.isComplete())); } _checkIfOnlyFeatureType = async (featureType) => { @@ -288,16 +270,12 @@ export class VectorStyle extends AbstractStyle { return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.LINE); } - _getIsPolygonsOnly = async () => { - return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.POLYGON); - } - _getFieldRange = (fieldName) => { return _.get(this._descriptor, ['__styleMeta', fieldName]); } getIcon = () => { - const styles = this.getProperties(); + const styles = this.getRawProperties(); const symbolId = this.arePointsSymbolizedAsCircles() ? undefined : this._descriptor.properties.symbol.options.symbolId; @@ -305,65 +283,54 @@ export class VectorStyle extends AbstractStyle { ); } - getLegendDetails(getFieldLabel, getFieldFormatter) { - const styles = this.getProperties(); - const styleProperties = Object.keys(styles).map(styleName => { - const { type, options } = styles[styleName]; + renderLegendDetails() { + const styles = this._getAllStyleProperties(); + const styleProperties = styles.map((style) => { return { - name: styleName, - type, - options, - range: options && options.field && options.field.name ? this._getFieldRange(options.field.name) : null, + // eslint-disable-next-line max-len + range: (style.isDynamic() && style.isComplete() && style.getField().getName()) ? this._getFieldRange(style.getField().getName()) : null, + style: style }; }); return ( ); } _getStyleFields() { return this.getDynamicPropertiesArray() - .map(({ styleName, options }) => { - const name = options.field.name; + .map(styleProperty => { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; - if (styleName === 'iconSize' + if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; isScaled = true; - } else if (styleName === 'iconOrientation') { - supportsFeatureState = false; - isScaled = false; - } else if ((styleName === vectorStyles.FILL_COLOR || styleName === vectorStyles.LINE_COLOR) - && options.useCustomColorRamp) { - supportsFeatureState = true; - isScaled = false; } else { - supportsFeatureState = true; - isScaled = true; + supportsFeatureState = styleProperty.supportsFeatureState(); + isScaled = styleProperty.isScaled(); } + const field = styleProperty.getField(); return { supportsFeatureState, isScaled, - name, - range: this._getFieldRange(name), - computedName: VectorStyle.getComputedFieldName(styleName, name), + name: field.getName(), + range: this._getFieldRange(field.getName()), + computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()), }; }); } @@ -472,13 +439,46 @@ export class VectorStyle extends AbstractStyle { } + arePointsSymbolizedAsCircles() { + return this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE; + } + + _makeField(fieldDescriptor) { + + if (!fieldDescriptor || !fieldDescriptor.name) { + return null; + } + + //fieldDescriptor.label is ignored. This is essentially cruft duplicating label-info from the metric-selection + //Ignore this custom label + if (fieldDescriptor.origin === FIELD_ORIGIN.SOURCE) { + return this._source.createField({ + fieldName: fieldDescriptor.name + }); + } else if (fieldDescriptor.origin === FIELD_ORIGIN.JOIN) { + let matchingField = null; + const joins = this._layer.getValidJoins(); + joins.find(join => { + const aggSource = join.getRightJoinSource(); + matchingField = aggSource.getMetricFieldForName(fieldDescriptor.name); + return !!matchingField; + }); + return matchingField; + } else { + throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); + } + + + } + _makeSizeProperty(descriptor, styleName) { if (!descriptor || !descriptor.options) { return new StaticSizeProperty({ size: 0 }, styleName); } else if (descriptor.type === StaticStyleProperty.type) { return new StaticSizeProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicSizeProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicSizeProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } @@ -490,7 +490,8 @@ export class VectorStyle extends AbstractStyle { } else if (descriptor.type === StaticStyleProperty.type) { return new StaticColorProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicColorProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicColorProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } @@ -502,7 +503,8 @@ export class VectorStyle extends AbstractStyle { } else if (descriptor.type === StaticStyleProperty.type) { return new StaticOrientationProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicOrientationProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicOrientationProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js index c0d76fadc01a5..a3020524a4e2a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js @@ -7,6 +7,35 @@ import { VectorStyle } from './vector_style'; import { DataRequest } from '../../util/data_request'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; +import { FIELD_ORIGIN } from '../../../../common/constants'; + +class MockField { + constructor({ fieldName }) { + this._fieldName = fieldName; + } + + getName() { + return this._fieldName; + } + + isValid() { + return !!this._fieldName; + } +} + +class MockSource { + + constructor({ supportedShapeTypes } = {}) { + this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPES); + } + getSupportedShapeTypes() { + return this._supportedShapeTypes; + } + createField({ fieldName }) { + return new MockField({ fieldName }); + } +} + describe('getDescriptorWithMissingStylePropsRemoved', () => { const fieldName = 'doIStillExist'; @@ -17,29 +46,32 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }, lineColor: { type: VectorStyle.STYLE_TYPE.DYNAMIC, - options: {} + options: { + 'field': { + 'name': fieldName, + 'origin': FIELD_ORIGIN.SOURCE + } + } }, iconSize: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: 'a color', - field: { name: fieldName } + field: { name: fieldName, origin: FIELD_ORIGIN.SOURCE } } } }; it('Should return no changes when next oridinal fields contain existing style property fields', () => { - const vectorStyle = new VectorStyle({ properties }); + const vectorStyle = new VectorStyle({ properties }, new MockSource()); - const nextOridinalFields = [ - { name: fieldName } - ]; + const nextOridinalFields = [new MockField({ fieldName })]; const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields); expect(hasChanges).toBe(false); }); it('Should clear missing fields when next oridinal fields do not contain existing style property fields', () => { - const vectorStyle = new VectorStyle({ properties }); + const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextOridinalFields = []; const { hasChanges, nextStyleDescriptor } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields); @@ -83,12 +115,6 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { describe('pluckStyleMetaFromSourceDataRequest', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return Object.values(VECTOR_SHAPE_TYPES); - } - }; - describe('has features', () => { it('Should identify when feature collection only contains points', async () => { const sourceDataRequest = new DataRequest({ @@ -110,7 +136,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], } }); - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -140,7 +166,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], } }); - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -183,12 +209,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { + origin: FIELD_ORIGIN.SOURCE, name: 'myDynamicFieldWithNoValues' } } } } - }, sourceMock); + }, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -205,12 +232,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { + origin: FIELD_ORIGIN.SOURCE, name: 'myDynamicField' } } } } - }, sourceMock); + }, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.myDynamicField).toEqual({ @@ -226,32 +254,24 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { describe('checkIfOnlyFeatureType', () => { describe('source supports single feature type', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return [VECTOR_SHAPE_TYPES.POINT]; - } - }; - it('isPointsOnly should be true when source feature type only supports points', async () => { - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource({ + supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT] + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(true); }); it('isLineOnly should be false when source feature type only supports points', async () => { - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource({ + supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT] + })); const isLineOnly = await vectorStyle._getIsLinesOnly(); expect(isLineOnly).toBe(false); }); }); describe('source supports multiple feature types', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return Object.values(VECTOR_SHAPE_TYPES); - } - }; - it('isPointsOnly should be true when data contains just points', async () => { const vectorStyle = new VectorStyle({ __styleMeta: { @@ -261,7 +281,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: false } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(true); }); @@ -275,7 +297,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: false } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(false); }); @@ -289,7 +313,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: true } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(false); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js index 3dfe83ac83d02..70cfb6939e0d2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js @@ -12,10 +12,6 @@ export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - } - static createDescriptor(options) { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = TileLayer.type; diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js index 42629e192c27d..dce9ed479a4d7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js @@ -14,6 +14,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { super(propertyKey, propertyName, rawValue, indexPattern); this._metricField = metricField; } + isFilterable() { return false; } @@ -22,10 +23,10 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { if (typeof this._rawValue === 'undefined') { return '-'; } - if (this._metricField.type === METRIC_TYPE.COUNT || this._metricField.type === METRIC_TYPE.UNIQUE_COUNT) { + if (this._metricField.getAggType() === METRIC_TYPE.COUNT || this._metricField.getAggType() === METRIC_TYPE.UNIQUE_COUNT) { return this._rawValue; } - const indexPatternField = this._indexPattern.fields.getByName(this._metricField.field); + const indexPatternField = this._indexPattern.fields.getByName(this._metricField.getESDocFieldName()); if (!indexPatternField) { return this._rawValue; } diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js index cc19521063f36..ed9b284c12826 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js @@ -38,12 +38,14 @@ export class JoinTooltipProperty extends TooltipProperty { for (let i = 0; i < this._leftInnerJoins.length; i++) { const rightSource = this._leftInnerJoins[i].getRightJoinSource(); - const esTooltipProperty = await rightSource.createESTooltipProperty( - rightSource.getTerm(), - this._tooltipProperty.getRawValue() - ); - if (esTooltipProperty) { - esFilters.push(...(await esTooltipProperty.getESFilters())); + const termField = rightSource.getTermField(); + try { + const esTooltipProperty = await termField.createTooltipProperty(this._tooltipProperty.getRawValue()); + if (esTooltipProperty) { + esFilters.push(...(await esTooltipProperty.getESFilters())); + } + } catch(e) { + console.error('Cannot create joined filter', e); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js new file mode 100644 index 0000000000000..2c0d08f86cfc0 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +let idCounter = 0; + +function generateNumericalId() { + const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; + idCounter = newId + 1; + return newId; +} + +export function assignFeatureIds(featureCollection) { + + // wrt https://github.com/elastic/kibana/issues/39317 + // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. + // This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. + // This is a work-around to avoid hitting such a worst-case + // This was tested as a suitable work-around for mapbox-gl 0.54 + // The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 + + // This only shuffles the id-assignment, _not_ the features in the collection + // The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. + const ids = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const id = generateNumericalId(); + ids.push(id); + } + + const randomizedIds = _.shuffle(ids); + const features = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const numericId = randomizedIds[i]; + const feature = featureCollection.features[i]; + features.push({ + type: 'Feature', + geometry: feature.geometry, // do not copy geometry, this object can be massive + properties: { + // preserve feature id provided by source so features can be referenced across fetches + [FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, + // create new object for properties so original is not polluted with kibana internal props + ...feature.properties, + }, + id: numericId, // Mapbox feature state id, must be integer + }); + } + + return { + type: 'FeatureCollection', + features + }; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js new file mode 100644 index 0000000000000..0678070f568a2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { assignFeatureIds } from './assign_feature_ids'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +const featureId = 'myFeature1'; + +test('should provide unique id when feature.id is not provided', () => { + const featureCollection = { + features: [ + { + properties: {} + }, + { + properties: {} + }, + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + const feature2 = updatedFeatureCollection.features[1]; + expect(typeof feature1.id).toBe('number'); + expect(typeof feature2.id).toBe('number'); + expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature2.id); +}); + +test('should preserve feature id when provided', () => { + const featureCollection = { + features: [ + { + id: featureId, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); +}); + +test('should preserve feature id for falsy value', () => { + const featureCollection = { + features: [ + { + id: 0, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0); +}); + +test('should not modify original feature properties', () => { + const featureProperties = {}; + const featureCollection = { + features: [ + { + id: featureId, + properties: featureProperties + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME); +}); + diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js new file mode 100644 index 0000000000000..610c704b34ec6 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import { isRefreshOnlyQuery } from './is_refresh_only_query'; + +const SOURCE_UPDATE_REQUIRED = true; +const NO_SOURCE_UPDATE_REQUIRED = false; + +export function updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { + const extentAware = source.isFilterByMapBounds(); + if (!extentAware) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const { buffer: previousBuffer } = prevMeta; + const { buffer: newBuffer } = nextMeta; + + if (!previousBuffer) { + return SOURCE_UPDATE_REQUIRED; + } + + if (_.isEqual(previousBuffer, newBuffer)) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const previousBufferGeometry = turf.bboxPolygon([ + previousBuffer.minLon, + previousBuffer.minLat, + previousBuffer.maxLon, + previousBuffer.maxLat + ]); + const newBufferGeometry = turf.bboxPolygon([ + newBuffer.minLon, + newBuffer.minLat, + newBuffer.maxLon, + newBuffer.maxLat + ]); + const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); + + const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); + return doesPreviousBufferContainNewBuffer && !isTrimmed + ? NO_SOURCE_UPDATE_REQUIRED + : SOURCE_UPDATE_REQUIRED; +} + +export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) { + + const timeAware = await source.isTimeAware(); + const refreshTimerAware = await source.isRefreshTimerAware(); + const extentAware = source.isFilterByMapBounds(); + const isFieldAware = source.isFieldAware(); + const isQueryAware = source.isQueryAware(); + const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); + + if ( + !timeAware && + !refreshTimerAware && + !extentAware && + !isFieldAware && + !isQueryAware && + !isGeoGridPrecisionAware + ) { + return (prevDataRequest && prevDataRequest.hasDataOrRequestInProgress()); + } + + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + let updateDueToTime = false; + if (timeAware) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } + + let updateDueToRefreshTimer = false; + if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { + updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); + } + + let updateDueToFields = false; + if (isFieldAware) { + updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); + } + + let updateDueToQuery = false; + let updateDueToFilters = false; + let updateDueToSourceQuery = false; + let updateDueToApplyGlobalQuery = false; + if (isQueryAware) { + updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; + updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { + updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); + updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); + } else { + // Global filters and query are not applied to layer search request so no re-fetch required. + // Exception is "Refresh" query. + updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); + } + } + + let updateDueToPrecisionChange = false; + if (isGeoGridPrecisionAware) { + updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); + } + + const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + + const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); + + return !updateDueToTime + && !updateDueToRefreshTimer + && !updateDueToExtentChange + && !updateDueToFields + && !updateDueToQuery + && !updateDueToFilters + && !updateDueToSourceQuery + && !updateDueToApplyGlobalQuery + && !updateDueToPrecisionChange + && !updateDueToSourceMetaChange; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js new file mode 100644 index 0000000000000..77359a6def48f --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; +import { DataRequest } from './data_request'; + +describe('updateDueToExtent', () => { + + it('should be false when the source is not extent aware', async () => { + const sourceMock = { + isFilterByMapBounds: () => { return false; } + }; + expect(updateDueToExtent(sourceMock)).toBe(false); + }); + + describe('source is extent aware', () => { + const sourceMock = { + isFilterByMapBounds: () => { return true; } + }; + + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })) + .toBe(false); + }); + + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); + + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent( + sourceMock, + { buffer: oldBuffer, areResultsTrimmed: true }, + { buffer: newBuffer } + )).toBe(true); + }); + + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent(sourceMock)).toBe(true); + }); + + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); + }); + }); +}); + +describe('canSkipSourceUpdate', () => { + const SOURCE_DATA_REQUEST_ID = 'foo'; + + describe('isQueryAware', () => { + + const queryAwareSourceMock = { + isTimeAware: () => { return false; }, + isRefreshTimerAware: () => { return false; }, + isFilterByMapBounds: () => { return false; }, + isFieldAware: () => { return false; }, + isQueryAware: () => { return true; }, + isGeoGridPrecisionAware: () => { return false; }, + }; + const prevFilters = []; + const prevQuery = { + language: 'kuery', + query: 'machine.os.keyword : "win 7"', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' + }; + + describe('applyGlobalQuery is false', () => { + + const prevApplyGlobalQuery = false; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + + describe('applyGlobalQuery is true', () => { + + const prevApplyGlobalQuery = true; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can not skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js new file mode 100644 index 0000000000000..393c290d69668 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +const VISIBILITY_FILTER_CLAUSE = ['all', + [ + '==', + ['get', FEATURE_VISIBLE_PROPERTY_NAME], + true + ] +]; + +const CLOSED_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] +]; + +const VISIBLE_CLOSED_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + CLOSED_SHAPE_MB_FILTER, +]; + +const ALL_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] +]; + +const VISIBLE_ALL_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + ALL_SHAPE_MB_FILTER, +]; + +const POINT_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] +]; + +const VISIBLE_POINT_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + POINT_MB_FILTER, +]; + +export function getFillFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER; +} + +export function getLineFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER; +} + +export function getPointFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 925ff963e05d4..57126bb7681b8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -10,65 +10,24 @@ import { AbstractLayer } from './layer'; import { VectorStyle } from './styles/vector/vector_style'; import { InnerJoin } from './joins/inner_join'; import { - GEO_JSON_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - LAYER_TYPE, - FIELD_ORIGIN, + LAYER_TYPE } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; -import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; - -const VISIBILITY_FILTER_CLAUSE = ['all', - [ - '==', - ['get', FEATURE_VISIBLE_PROPERTY_NAME], - true - ] -]; - -const FILL_LAYER_MB_FILTER = [ - ...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] - ] -]; - -const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] - ] -]; - -const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] - ] -]; - - -let idCounter = 0; - -function generateNumericalId() { - const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; - idCounter = newId + 1; - return newId; -} - +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { assignFeatureIds } from './util/assign_feature_ids'; +import { + getFillFilterExpression, + getLineFilterExpression, + getPointFilterExpression, +} from './util/mb_filter_expressions'; export class VectorLayer extends AbstractLayer { @@ -91,9 +50,11 @@ export class VectorLayer extends AbstractLayer { this._joins = []; if (options.layerDescriptor.joins) { options.layerDescriptor.joins.forEach((joinDescriptor) => { - this._joins.push(new InnerJoin(joinDescriptor, this._source.getInspectorAdapters())); + const join = new InnerJoin(joinDescriptor, this._source); + this._joins.push(join); }); } + this._style = new VectorStyle(this._descriptor.style, this._source, this); } destroy() { @@ -115,6 +76,10 @@ export class VectorLayer extends AbstractLayer { }); } + _hasJoins() { + return this.getValidJoins().length > 0; + } + isDataLoaded() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { @@ -181,26 +146,8 @@ export class VectorLayer extends AbstractLayer { return this._style.getDynamicPropertiesArray().length > 0; } - getLegendDetails() { - const getFieldLabel = async fieldName => { - const ordinalFields = await this._getOrdinalFields(); - const field = ordinalFields.find(({ name }) => { - return name === fieldName; - }); - - return field ? field.label : fieldName; - }; - - const getFieldFormatter = async field => { - const source = this._getFieldSource(field); - if (!source) { - return null; - } - - return await source.getFieldFormatter(field.name); - }; - - return this._style.getLegendDetails(getFieldLabel, getFieldFormatter); + renderLegendDetails() { + return this._style.renderLegendDetails(); } _getBoundsBasedOnData() { @@ -241,46 +188,24 @@ export class VectorLayer extends AbstractLayer { return this._source.getDisplayName(); } - async getDateFields() { - const timeFields = await this._source.getDateFields(); - return timeFields.map(({ label, name }) => { - return { - label, - name, - origin: SOURCE_DATA_ID_ORIGIN - }; - }); + return await this._source.getDateFields(); } - async getNumberFields() { - const numberFields = await this._source.getNumberFields(); - const numberFieldOptions = numberFields.map(({ label, name }) => { - return { - label, - name, - origin: FIELD_ORIGIN.SOURCE - }; - }); + const numberFieldOptions = await this._source.getNumberFields(); const joinFields = []; this.getValidJoins().forEach(join => { - const fields = join.getJoinFields().map(joinField => { - return { - ...joinField, - origin: FIELD_ORIGIN.JOIN, - }; - }); + const fields = join.getJoinFields(); joinFields.push(...fields); }); - return [...numberFieldOptions, ...joinFields]; } - async _getOrdinalFields() { + async getOrdinalFields() { return [ - ... await this.getDateFields(), - ... await this.getNumberFields() + ...await this.getDateFields(), + ...await this.getNumberFields() ]; } @@ -304,109 +229,31 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - async _canSkipSourceUpdate(source, sourceDataId, nextMeta) { - - const timeAware = await source.isTimeAware(); - const refreshTimerAware = await source.isRefreshTimerAware(); - const extentAware = source.isFilterByMapBounds(); - const isFieldAware = source.isFieldAware(); - const isQueryAware = source.isQueryAware(); - const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); - if ( - !timeAware && - !refreshTimerAware && - !extentAware && - !isFieldAware && - !isQueryAware && - !isGeoGridPrecisionAware - ) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()); - } - - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - if (!sourceDataRequest) { - return false; - } - const prevMeta = sourceDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - let updateDueToTime = false; - if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - } - - let updateDueToRefreshTimer = false; - if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { - updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); - } - - let updateDueToFields = false; - if (isFieldAware) { - updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); - } - - let updateDueToQuery = false; - let updateDueToFilters = false; - let updateDueToSourceQuery = false; - let updateDueToApplyGlobalQuery = false; - if (isQueryAware) { - updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; - updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); - if (nextMeta.applyGlobalQuery) { - updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); - updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); - } else { - // Global filters and query are not applied to layer search request so no re-fetch required. - // Exception is "Refresh" query. - updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); - } - } - - let updateDueToPrecisionChange = false; - if (isGeoGridPrecisionAware) { - updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); - } - - const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta); - - const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); - - return !updateDueToTime - && !updateDueToRefreshTimer - && !updateDueToExtentChange - && !updateDueToFields - && !updateDueToQuery - && !updateDueToFilters - && !updateDueToSourceQuery - && !updateDueToApplyGlobalQuery - && !updateDueToPrecisionChange - && !updateDueToSourceMetaChange; - } async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); - const requestToken = Symbol(`layer-join-refresh:${ this.getId()} - ${sourceDataId}`); - + const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); const searchFilters = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters); - if (canSkip) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - const propertiesMap = sourceDataRequest ? sourceDataRequest.getData() : null; + const prevDataRequest = this._findDataRequestForSource(sourceDataId); + + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { dataHasChanged: false, join: join, - propertiesMap: propertiesMap + propertiesMap: prevDataRequest.getData() }; } @@ -418,7 +265,7 @@ export class VectorLayer extends AbstractLayer { } = await joinSource.getPropertiesMap( searchFilters, leftSourceName, - join.getLeftFieldName(), + join.getLeftField().getName(), registerCancelCallback.bind(null, requestToken)); stopLoading(sourceDataId, requestToken, propertiesMap); return { @@ -450,9 +297,7 @@ export class VectorLayer extends AbstractLayer { const fieldNames = [ ...this._source.getFieldNames(), ...this._style.getSourceFieldNames(), - ...this.getValidJoins().map(join => { - return join.getLeftFieldName(); - }) + ...this.getValidJoins().map(join => join.getLeftField().getName()) ]; return { @@ -484,9 +329,8 @@ export class VectorLayer extends AbstractLayer { let isFeatureVisible = true; for (let j = 0; j < joinStates.length; j++) { const joinState = joinStates[j]; - const InnerJoin = joinState.join; - const rightMetricFields = InnerJoin.getRightMetricFields(); - const canJoinOnCurrent = InnerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap, rightMetricFields); + const innerJoin = joinState.join; + const canJoinOnCurrent = innerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap); isFeatureVisible = isFeatureVisible && canJoinOnCurrent; } @@ -506,29 +350,34 @@ export class VectorLayer extends AbstractLayer { startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { - const requestToken = Symbol(`layer-source-refresh:${ this.getId()} - source`); + const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); - const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); - if (canSkip) { - const sourceDataRequest = this.getSourceDataRequest(); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkipFetch = await canSkipSourceUpdate({ + source: this._source, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { refreshed: false, - featureCollection: sourceDataRequest.getData() + featureCollection: prevDataRequest.getData() }; } try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); const layerName = await this.getDisplayName(); - const { data: featureCollection, meta } = + const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback.bind(null, requestToken) ); - this._assignIdsToFeatures(featureCollection); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, layerFeatureCollection, meta); return { refreshed: true, - featureCollection: featureCollection + featureCollection: layerFeatureCollection }; } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -540,44 +389,21 @@ export class VectorLayer extends AbstractLayer { } } - _assignIdsToFeatures(featureCollection) { - - //wrt https://github.com/elastic/kibana/issues/39317 - // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. - //This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. - //This is a work-around to avoid hitting such a worst-case - //This was tested as a suitable work-around for mapbox-gl 0.54 - //The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 - - //This only shuffles the id-assignment, _not_ the features in the collection - //The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. - const ids = []; - for (let i = 0; i < featureCollection.features.length; i++) { - const id = generateNumericalId(); - ids.push(id); - } - - const randomizedIds = _.shuffle(ids); - for (let i = 0; i < featureCollection.features.length; i++) { - const id = randomizedIds[i]; - const feature = featureCollection.features[i]; - feature.id = id; // Mapbox feature state id, must be integer - } - } - async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } const sourceResult = await this._syncSource(syncContext); - if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) { + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this._hasJoins()) { return; } const joinStates = await this._syncJoins(syncContext); await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); - } _getSourceFeatureCollection() { @@ -647,7 +473,11 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(pointLayerId)) { + mbMap.setFilter(pointLayerId, filterExpr); } this._style.setMBPaintPropertiesForPoints({ @@ -668,7 +498,11 @@ export class VectorLayer extends AbstractLayer { type: 'symbol', source: sourceId, }); - mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(symbolLayerId)) { + mbMap.setFilter(symbolLayerId, filterExpr); } this._style.setMBSymbolPropertiesForPoints({ @@ -682,6 +516,7 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { mbMap.addLayer({ id: fillLayerId, @@ -689,7 +524,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER); } if (!mbMap.getLayer(lineLayerId)) { mbMap.addLayer({ @@ -698,7 +532,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER); } this._style.setMBPaintProperties({ alpha: this.getAlpha(), @@ -708,9 +541,18 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); + mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const fillFilterExpr = getFillFilterExpression(hasJoins); + if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { + mbMap.setFilter(fillLayerId, fillFilterExpr); + } + this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const lineFilterExpr = getLineFilterExpression(hasJoins); + if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { + mbMap.setFilter(lineLayerId, lineFilterExpr); + } } _syncStylePropertiesWithMb(mbMap) { @@ -770,7 +612,7 @@ export class VectorLayer extends AbstractLayer { const tooltipProperty = tooltipsFromSource[i]; const matchingJoins = []; for (let j = 0; j < this._joins.length; j++) { - if (this._joins[j].getLeftFieldName() === tooltipProperty.getPropertyKey()) { + if (this._joins[j].getLeftField().getName() === tooltipProperty.getPropertyKey()) { matchingJoins.push(this._joins[j]); } } @@ -806,28 +648,4 @@ export class VectorLayer extends AbstractLayer { return feature.properties[FEATURE_ID_PROPERTY_NAME] === id; }); } - - _getFieldSource(field) { - if (!field) { - return null; - } - - if (field.origin === FIELD_ORIGIN.SOURCE) { - return this._source; - } - - const join = this.getValidJoins().find(join => { - const matchingField = join.getJoinFields().find(joinField => { - return joinField.name === field.name; - }); - return !!matchingField; - }); - - if (!join) { - return null; - } - - return join.getRightJoinSource(); - } - } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js deleted file mode 100644 index 0a07582c57856..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js +++ /dev/null @@ -1,185 +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. - */ - -jest.mock('./joins/inner_join', () => ({ - InnerJoin: Object -})); - -jest.mock('./tooltips/join_tooltip_property', () => ({ - JoinTooltipProperty: Object -})); - -import { VectorLayer } from './vector_layer'; - -describe('_canSkipSourceUpdate', () => { - const SOURCE_DATA_REQUEST_ID = 'foo'; - - describe('isQueryAware', () => { - - const queryAwareSourceMock = { - isTimeAware: () => { return false; }, - isRefreshTimerAware: () => { return false; }, - isFilterByMapBounds: () => { return false; }, - isFieldAware: () => { return false; }, - isQueryAware: () => { return true; }, - isGeoGridPrecisionAware: () => { return false; }, - }; - const prevFilters = []; - const prevQuery = { - language: 'kuery', - query: 'machine.os.keyword : "win 7"', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' - }; - - describe('applyGlobalQuery is false', () => { - - const prevApplyGlobalQuery = false; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - - describe('applyGlobalQuery is true', () => { - - const prevApplyGlobalQuery = true; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can not skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js index d714ac3b092f6..7b7cf76cd365c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js @@ -25,10 +25,6 @@ export class VectorTileLayer extends TileLayer { static type = LAYER_TYPE.VECTOR_TILE; - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - } - static createDescriptor(options) { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js index fca277515affd..000a93ee8752f 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/map.js +++ b/x-pack/legacy/plugins/maps/public/reducers/map.js @@ -41,6 +41,9 @@ import { SET_SCROLL_ZOOM, SET_MAP_INIT_ERROR, UPDATE_DRAW_STATE, + SET_INTERACTIVE, + DISABLE_TOOLTIP_CONTROL, + HIDE_TOOLBAR_OVERLAY, } from '../actions/map_actions'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; @@ -58,12 +61,13 @@ const updateLayerInList = (state, layerId, attribute, newValue) => { ...layerList[layerIdx], // Update layer w/ new value. If no value provided, toggle boolean value // allow empty strings, 0-value - [attribute]: (newValue || newValue === '' || newValue === 0) ? newValue : !layerList[layerIdx][attribute] + [attribute]: + newValue || newValue === '' || newValue === 0 ? newValue : !layerList[layerIdx][attribute], }; const updatedList = [ ...layerList.slice(0, layerIdx), updatedLayer, - ...layerList.slice(layerIdx + 1) + ...layerList.slice(layerIdx + 1), ]; return { ...state, layerList: updatedList }; }; @@ -76,12 +80,12 @@ const updateLayerSourceDescriptorProp = (state, layerId, propName, value) => { sourceDescriptor: { ...layerList[layerIdx].sourceDescriptor, [propName]: value, - } + }, }; const updatedList = [ ...layerList.slice(0, layerIdx), updatedLayer, - ...layerList.slice(layerIdx + 1) + ...layerList.slice(layerIdx + 1), ]; return { ...state, layerList: updatedList }; }; @@ -95,7 +99,7 @@ const INITIAL_STATE = { zoom: 4, center: { lon: -100.41, - lat: 32.82 + lat: 32.82, }, scrollZoom: true, extent: null, @@ -105,7 +109,10 @@ const INITIAL_STATE = { filters: [], refreshConfig: null, refreshTimerLastTriggeredAt: null, - drawState: null + drawState: null, + disableInteractive: false, + disableTooltipControl: false, + hideToolbarOverlay: false }, selectedLayerId: null, __transientLayerId: null, @@ -113,7 +120,6 @@ const INITIAL_STATE = { waitingForMapReadyLayerList: [], }; - export function map(state = INITIAL_STATE, action) { switch (action.type) { case UPDATE_DRAW_STATE: @@ -121,8 +127,8 @@ export function map(state = INITIAL_STATE, action) { ...state, mapState: { ...state.mapState, - drawState: action.drawState - } + drawState: action.drawState, + }, }; case REMOVE_TRACKED_LAYER_STATE: return removeTrackedLayerState(state, action.layerId); @@ -133,7 +139,7 @@ export function map(state = INITIAL_STATE, action) { case SET_TOOLTIP_STATE: return { ...state, - tooltipState: action.tooltipState + tooltipState: action.tooltipState, }; case SET_MOUSE_COORDINATES: return { @@ -142,25 +148,25 @@ export function map(state = INITIAL_STATE, action) { ...state.mapState, mouseCoordinates: { lat: action.lat, - lon: action.lon - } - } + lon: action.lon, + }, + }, }; case CLEAR_MOUSE_COORDINATES: return { ...state, mapState: { ...state.mapState, - mouseCoordinates: null - } + mouseCoordinates: null, + }, }; case SET_GOTO: return { ...state, goto: { center: action.center, - bounds: action.bounds - } + bounds: action.bounds, + }, }; case CLEAR_GOTO: return { @@ -181,10 +187,10 @@ export function map(state = INITIAL_STATE, action) { { ...layerList[layerIdx], __isInErrorState: action.isInErrorState, - __errorMessage: action.errorMessage + __errorMessage: action.errorMessage, }, - ...layerList.slice(layerIdx + 1) - ] + ...layerList.slice(layerIdx + 1), + ], }; case UPDATE_SOURCE_DATA_REQUEST: return updateSourceDataRequest(state, action); @@ -226,7 +232,7 @@ export function map(state = INITIAL_STATE, action) { query, timeFilters, filters, - } + }, }; case SET_REFRESH_CONFIG: const { isPaused, interval } = action; @@ -237,16 +243,16 @@ export function map(state = INITIAL_STATE, action) { refreshConfig: { isPaused, interval, - } - } + }, + }, }; case TRIGGER_REFRESH_TIMER: return { ...state, mapState: { ...state.mapState, - refreshTimerLastTriggeredAt: (new Date()).toISOString(), - } + refreshTimerLastTriggeredAt: new Date().toISOString(), + }, }; case SET_SELECTED_LAYER: const selectedMatch = state.layerList.find(layer => layer.id === action.selectedLayerId); @@ -255,16 +261,23 @@ export function map(state = INITIAL_STATE, action) { const transientMatch = state.layerList.find(layer => layer.id === action.transientLayerId); return { ...state, __transientLayerId: transientMatch ? action.transientLayerId : null }; case UPDATE_LAYER_ORDER: - return { ...state, layerList: action.newLayerOrder.map(layerNumber => state.layerList[layerNumber]) }; + return { + ...state, + layerList: action.newLayerOrder.map(layerNumber => state.layerList[layerNumber]), + }; case UPDATE_LAYER_PROP: return updateLayerInList(state, action.id, action.propName, action.newValue); case UPDATE_SOURCE_PROP: return updateLayerSourceDescriptorProp(state, action.layerId, action.propName, action.value); case SET_JOINS: - const layerDescriptor = state.layerList.find(descriptor => descriptor.id === action.layer.getId()); + const layerDescriptor = state.layerList.find( + descriptor => descriptor.id === action.layer.getId() + ); if (layerDescriptor) { const newLayerDescriptor = { ...layerDescriptor, joins: action.joins.slice() }; - const index = state.layerList.findIndex(descriptor => descriptor.id === action.layer.getId()); + const index = state.layerList.findIndex( + descriptor => descriptor.id === action.layer.getId() + ); const newLayerList = state.layerList.slice(); newLayerList[index] = newLayerDescriptor; return { ...state, layerList: newLayerList }; @@ -273,35 +286,28 @@ export function map(state = INITIAL_STATE, action) { case ADD_LAYER: return { ...state, - layerList: [ - ...state.layerList, - action.layer - ] + layerList: [...state.layerList, action.layer], }; case REMOVE_LAYER: return { - ...state, layerList: [...state.layerList.filter( - ({ id }) => id !== action.id)] + ...state, + layerList: [...state.layerList.filter(({ id }) => id !== action.id)], }; case ADD_WAITING_FOR_MAP_READY_LAYER: return { ...state, - waitingForMapReadyLayerList: [ - ...state.waitingForMapReadyLayerList, - action.layer - ] + waitingForMapReadyLayerList: [...state.waitingForMapReadyLayerList, action.layer], }; case CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST: return { ...state, - waitingForMapReadyLayerList: [] + waitingForMapReadyLayerList: [], }; case TOGGLE_LAYER_VISIBLE: return updateLayerInList(state, action.layerId, 'visible'); case UPDATE_LAYER_STYLE: const styleLayerId = action.layerId; - return updateLayerInList(state, styleLayerId, 'style', - { ...action.style }); + return updateLayerInList(state, styleLayerId, 'style', { ...action.style }); case SET_LAYER_STYLE_META: const { layerId, styleMeta } = action; const index = getLayerIndex(state.layerList, layerId); @@ -309,19 +315,46 @@ export function map(state = INITIAL_STATE, action) { return state; } - return updateLayerInList(state, layerId, 'style', { ...state.layerList[index].style, __styleMeta: styleMeta }); + return updateLayerInList(state, layerId, 'style', { + ...state.layerList[index].style, + __styleMeta: styleMeta, + }); case SET_SCROLL_ZOOM: return { ...state, mapState: { ...state.mapState, scrollZoom: action.scrollZoom, - } + }, }; case SET_MAP_INIT_ERROR: return { ...state, - mapInitError: action.errorMessage + mapInitError: action.errorMessage, + }; + case SET_INTERACTIVE: + return { + ...state, + mapState: { + ...state.mapState, + disableInteractive: action.disableInteractive, + }, + }; + case DISABLE_TOOLTIP_CONTROL: + return { + ...state, + mapState: { + ...state.mapState, + disableTooltipControl: action.disableTooltipControl, + }, + }; + case HIDE_TOOLBAR_OVERLAY: + return { + ...state, + mapState: { + ...state.mapState, + hideToolbarOverlay: action.hideToolbarOverlay, + }, }; default: return state; @@ -329,7 +362,6 @@ export function map(state = INITIAL_STATE, action) { } function findDataRequest(layerDescriptor, dataRequestAction) { - if (!layerDescriptor.__dataRequests) { return; } @@ -339,18 +371,18 @@ function findDataRequest(layerDescriptor, dataRequestAction) { }); } - function updateWithDataRequest(state, action) { let dataRequest = getValidDataRequest(state, action, false); const layerRequestingData = findLayerById(state, action.layerId); if (!dataRequest) { dataRequest = { - dataId: action.dataId + dataId: action.dataId, }; layerRequestingData.__dataRequests = [ - ...(layerRequestingData.__dataRequests - ? layerRequestingData.__dataRequests : []), dataRequest ]; + ...(layerRequestingData.__dataRequests ? layerRequestingData.__dataRequests : []), + dataRequest, + ]; } dataRequest.dataMetaAtStart = action.meta; dataRequest.dataRequestToken = action.requestToken; @@ -358,13 +390,12 @@ function updateWithDataRequest(state, action) { return { ...state, layerList }; } - function updateSourceDataRequest(state, action) { const layerDescriptor = findLayerById(state, action.layerId); if (!layerDescriptor) { return state; } - const dataRequest = layerDescriptor.__dataRequests.find(dataRequest => { + const dataRequest = layerDescriptor.__dataRequests.find(dataRequest => { return dataRequest.dataId === SOURCE_DATA_ID_ORIGIN; }); if (!dataRequest) { @@ -375,10 +406,11 @@ function updateSourceDataRequest(state, action) { return resetDataRequest(state, action, dataRequest); } - function updateWithDataResponse(state, action) { const dataRequest = getValidDataRequest(state, action); - if (!dataRequest) { return state; } + if (!dataRequest) { + return state; + } dataRequest.data = action.data; dataRequest.dataMeta = { ...dataRequest.dataMetaAtStart, ...action.meta }; @@ -388,7 +420,9 @@ function updateWithDataResponse(state, action) { function resetDataRequest(state, action, request) { const dataRequest = request || getValidDataRequest(state, action); - if (!dataRequest) { return state; } + if (!dataRequest) { + return state; + } dataRequest.dataRequestToken = null; dataRequest.dataId = action.dataId; @@ -429,7 +463,7 @@ function trackCurrentLayerState(state, layerId) { } function removeTrackedLayerState(state, layerId) { - const layer = findLayerById(state, layerId); + const layer = findLayerById(state, layerId); if (!layer) { return state; } @@ -439,7 +473,7 @@ function removeTrackedLayerState(state, layerId) { return { ...state, - layerList: replaceInLayerList(state.layerList, layerId, copyLayer) + layerList: replaceInLayerList(state.layerList, layerId, copyLayer), }; } @@ -459,7 +493,7 @@ function rollbackTrackedLayerState(state, layerId) { return { ...state, - layerList: replaceInLayerList(state.layerList, layerId, rolledbackLayer) + layerList: replaceInLayerList(state.layerList, layerId, rolledbackLayer), }; } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 4f561c9391d4d..0b13db994193b 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -11,24 +11,22 @@ import { VectorTileLayer } from '../layers/vector_tile_layer'; import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; -import { VectorStyle } from '../layers/styles/vector/vector_style'; -import { HeatmapStyle } from '../layers/styles/heatmap/heatmap_style'; import { timefilter } from 'ui/timefilter'; import { getInspectorAdapters } from '../reducers/non_serializable_instances'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; function createLayerInstance(layerDescriptor, inspectorAdapters) { const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); - const style = createStyleInstance(layerDescriptor.style, source); + switch (layerDescriptor.type) { case TileLayer.type: - return new TileLayer({ layerDescriptor, source, style }); + return new TileLayer({ layerDescriptor, source }); case VectorLayer.type: - return new VectorLayer({ layerDescriptor, source, style }); + return new VectorLayer({ layerDescriptor, source }); case VectorTileLayer.type: - return new VectorTileLayer({ layerDescriptor, source, style }); + return new VectorTileLayer({ layerDescriptor, source }); case HeatmapLayer.type: - return new HeatmapLayer({ layerDescriptor, source, style }); + return new HeatmapLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } @@ -44,25 +42,6 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new Source(sourceDescriptor, inspectorAdapters); } - -function createStyleInstance(styleDescriptor, source) { - - if (!styleDescriptor || !styleDescriptor.type) { - return null; - } - - switch (styleDescriptor.type) { - case 'TILE'://backfill for old tilestyles. - return null; - case VectorStyle.type: - return new VectorStyle(styleDescriptor, source); - case HeatmapStyle.type: - return new HeatmapStyle(styleDescriptor); - default: - throw new Error(`Unrecognized styleType ${styleDescriptor.type}`); - } -} - export const getTooltipState = ({ map }) => { return map.tooltipState; }; @@ -87,6 +66,12 @@ export const getWaitingForMapReadyLayerListRaw = ({ map }) => map.waitingForMapR export const getScrollZoom = ({ map }) => map.mapState.scrollZoom; +export const isInteractiveDisabled = ({ map }) => map.mapState.disableInteractive; + +export const isTooltipControlDisabled = ({ map }) => map.mapState.disableTooltipControl; + +export const isToolbarOverlayHidden = ({ map }) => map.mapState.hideToolbarOverlay; + export const getMapExtent = ({ map }) => map.mapState.extent ? map.mapState.extent : {}; diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js index 0d318c41a7fd1..1c875322f2343 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { EMS_FILE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { EMS_FILE, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; function getSavedObjectsClient(server, callCluster) { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; @@ -32,8 +32,16 @@ function getUniqueLayerCounts(layerCountsList, mapsCount) { }, {}); } -export function buildMapsTelemetry(savedObjects, settings) { - const layerLists = savedObjects +function getIndexPatternsWithGeoFieldCount(indexPatterns) { + const fieldLists = indexPatterns.map(indexPattern => JSON.parse(indexPattern.attributes.fields)); + const fieldListsWithGeoFields = fieldLists.filter(fields => { + return fields.some(field => (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE)); + }); + return fieldListsWithGeoFields.length; +} + +export function buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }) { + const layerLists = mapSavedObjects .map(savedMapObject => JSON.parse(savedMapObject.attributes.layerListJSON)); const mapsCount = layerLists.length; @@ -57,8 +65,11 @@ export function buildMapsTelemetry(savedObjects, settings) { const dataSourcesCountSum = _.sum(dataSourcesCount); const layersCountSum = _.sum(layersCount); + + const indexPatternsWithGeoFieldCount = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); return { settings, + indexPatternsWithGeoFieldCount, // Total count of maps mapsTotalCount: mapsCount, // Time of capture @@ -88,24 +99,23 @@ export function buildMapsTelemetry(savedObjects, settings) { }; } -async function getSavedObjects(savedObjectsClient) { - const gisMapsSavedObject = await savedObjectsClient.find({ - type: MAP_SAVED_OBJECT_TYPE - }); - return _.get(gisMapsSavedObject, 'saved_objects'); +async function getMapSavedObjects(savedObjectsClient) { + const mapsSavedObjects = await savedObjectsClient.find({ type: MAP_SAVED_OBJECT_TYPE }); + return _.get(mapsSavedObjects, 'saved_objects', []); +} + +async function getIndexPatternSavedObjects(savedObjectsClient) { + const indexPatternSavedObjects = await savedObjectsClient.find({ type: 'index-pattern' }); + return _.get(indexPatternSavedObjects, 'saved_objects', []); } export async function getMapsTelemetry(server, callCluster) { const savedObjectsClient = getSavedObjectsClient(server, callCluster); - const savedObjects = await getSavedObjects(savedObjectsClient); + const mapSavedObjects = await getMapSavedObjects(savedObjectsClient); + const indexPatternSavedObjects = await getIndexPatternSavedObjects(savedObjectsClient); const settings = { showMapVisualizationTypes: server.config().get('xpack.maps.showMapVisualizationTypes') }; - const mapsTelemetry = buildMapsTelemetry(savedObjects, settings); - - return await savedObjectsClient.create('maps-telemetry', - mapsTelemetry, { - id: 'maps-telemetry', - overwrite: true, - }); + const mapsTelemetry = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }); + return await savedObjectsClient.create('maps-telemetry', mapsTelemetry, { id: 'maps-telemetry', overwrite: true }); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 4f2b983a54028..c5b976e54865e 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as savedObjectsPayload from - './test_resources/sample_saved_objects.json'; +import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; +import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsTelemetry } from './maps_telemetry'; describe('buildMapsTelemetry', () => { @@ -15,10 +15,10 @@ describe('buildMapsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { - const gisMaps = []; - const result = buildMapsTelemetry(gisMaps, settings); + const result = buildMapsTelemetry({ mapSavedObjects: [], indexPatternSavedObjects: [], settings }); expect(result).toMatchObject({ + indexPatternsWithGeoFieldCount: 0, attributesPerMap: { dataSourcesCount: { avg: 0, @@ -42,10 +42,10 @@ describe('buildMapsTelemetry', () => { test('returns expected telemetry data from saved objects', async () => { - const gisMaps = savedObjectsPayload.saved_objects; - const result = buildMapsTelemetry(gisMaps, settings); + const result = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }); expect(result).toMatchObject({ + indexPatternsWithGeoFieldCount: 2, attributesPerMap: { dataSourcesCount: { avg: 2.6666666666666665, diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index c0ac5a781b796..c4d755b5908f0 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -7,10 +7,10 @@ import _ from 'lodash'; import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task'; -export function initTelemetryCollection(server) { +export function initTelemetryCollection(usageCollection, server) { registerMapsTelemetryTask(server); scheduleTask(server); - registerMapsUsageCollector(server); + registerMapsUsageCollector(usageCollection, server); } async function isTaskManagerReady(server) { @@ -81,9 +81,8 @@ export function buildCollectorObj(server) { }; } -export function registerMapsUsageCollector(server) { +export function registerMapsUsageCollector(usageCollection, server) { const collectorObj = buildCollectorObj(server); - const mapsUsageCollector = server.usage.collectorSet - .makeUsageCollector(collectorObj); - server.usage.collectorSet.register(mapsUsageCollector); + const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj); + usageCollection.registerCollector(mapsUsageCollector); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json new file mode 100644 index 0000000000000..bb30a60f6d69f --- /dev/null +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json @@ -0,0 +1,45 @@ +[ + { + "attributes": { + "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", + "timeFieldName": "ORIG_DATE", + "title": "indexpattern-with-geoshape" + }, + "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T16:54:46.405Z", + "version": "Wzg0LDFd" + }, + { + "attributes": { + "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "indexpattern-with-geopoint" + }, + "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T20:05:37.607Z", + "version": "WzExMSwxXQ==" + }, + { + "attributes": { + "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "indexpattern-without-geo" + }, + "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T20:05:37.607Z", + "version": "WzExMSwxXQ==" + } +] diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json new file mode 100644 index 0000000000000..5bfe8ae38cac9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json @@ -0,0 +1,128 @@ +[ + { + "type": "gis-map", + "id": "37b08d60-25b0-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "Italy Map", + "description": "", + "mapStateJSON": "{\"zoom\":4.82,\"center\":{\"lon\":11.41545,\"lat\":42.0865},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"language\":\"lucene\",\"query\":\"\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + -5.29778, + 51.54155 + ], + [ + -5.29778, + 30.98066 + ], + [ + 28.12868, + 30.98066 + ], + [ + 28.12868, + 51.54155 + ], + [ + -5.29778, + 51.54155 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:30:39.030Z", + "version": 1 + }, + { + "type": "gis-map", + "id": "5c061dc0-25af-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "France Map", + "description": "", + "mapStateJSON": "{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + -59.97005, + 63.9123 + ], + [ + -59.97005, + 11.25616 + ], + [ + 27.36184, + 11.25616 + ], + [ + 27.36184, + 63.9123 + ], + [ + -59.97005, + 63.9123 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:24:30.492Z", + "version": 1 + }, + { + "type": "gis-map", + "id": "b853d5f0-25ae-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "Canada Map", + "description": "", + "mapStateJSON": "{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + 163.37506, + 77.35215 + ], + [ + 163.37506, + -46.80667 + ], + [ + 19.2731, + -46.80667 + ], + [ + 19.2731, + 77.35215 + ], + [ + 163.37506, + 77.35215 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:19:55.855Z", + "version": 1 + } +] diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json deleted file mode 100644 index f5693027f585d..0000000000000 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "page":1, - "per_page":20, - "total":3, - "saved_objects":[ - { - "type":"gis-map", - "id":"37b08d60-25b0-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"Italy Map", - "description":"", - "mapStateJSON":"{\"zoom\":4.82,\"center\":{\"lon\":11.41545,\"lat\":42.0865},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"language\":\"lucene\",\"query\":\"\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - -5.29778, - 51.54155 - ], - [ - -5.29778, - 30.98066 - ], - [ - 28.12868, - 30.98066 - ], - [ - 28.12868, - 51.54155 - ], - [ - -5.29778, - 51.54155 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:30:39.030Z", - "version":1 - }, - { - "type":"gis-map", - "id":"5c061dc0-25af-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"France Map", - "description":"", - "mapStateJSON":"{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - -59.97005, - 63.9123 - ], - [ - -59.97005, - 11.25616 - ], - [ - 27.36184, - 11.25616 - ], - [ - 27.36184, - 63.9123 - ], - [ - -59.97005, - 63.9123 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:24:30.492Z", - "version":1 - }, - { - "type":"gis-map", - "id":"b853d5f0-25ae-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"Canada Map", - "description":"", - "mapStateJSON":"{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - 163.37506, - 77.35215 - ], - [ - 163.37506, - -46.80667 - ], - [ - 19.2731, - -46.80667 - ], - [ - 19.2731, - 77.35215 - ], - [ - 163.37506, - 77.35215 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:19:55.855Z", - "version":1 - } - ] -} diff --git a/x-pack/legacy/plugins/maps/server/test_utils/index.js b/x-pack/legacy/plugins/maps/server/test_utils/index.js index 13b7c56d6fc8b..e9f97101759f0 100644 --- a/x-pack/legacy/plugins/maps/server/test_utils/index.js +++ b/x-pack/legacy/plugins/maps/server/test_utils/index.js @@ -40,12 +40,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => ({ get: () => '' }), log: () => undefined }); diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index f9d9b6b0161e2..9e1b992eec907 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index 8fdc0d13a78a9..52259d8748a95 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectAttributes } from 'src/core/server/types'; -import { Datafeed, Job } from '../../public/jobs/new_job/common/job_creator/configs'; +import { Datafeed, Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; export interface ModuleJob { id: string; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts index 23152afe0af2f..df62d19b6d27b 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; + export interface ValidationMessage { id: string; } @@ -39,3 +41,5 @@ export function validateModelMemoryLimitUnits( export function processCreatedBy(customSettings: { created_by?: string }): void; export function mlFunctionToESAggregation(functionName: string): string | null; + +export function isModelPlotEnabled(job: Job, detectorIndex: number, entityFields: any[]): boolean; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 90a00d40d17b1..999eb44b372bc 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -12,7 +12,7 @@ import numeral from '@elastic/numeral'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; -import { CREATED_BY_LABEL } from '../../public/jobs/new_job/common/job_creator/util/constants'; +import { CREATED_BY_LABEL } from '../../public/application/jobs/new_job/common/job_creator/util/constants'; // work out the default frequency based on the bucket_span in seconds export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 51f7b88315f85..90e1e748492cb 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -28,7 +28,7 @@ export const ml = (kibana: any) => { publicDir: resolve(__dirname, 'public'), uiExports: { - managementSections: ['plugins/ml/management'], + managementSections: ['plugins/ml/application/management'], app: { title: i18n.translate('xpack.ml.mlNavTitle', { defaultMessage: 'Machine Learning', @@ -36,12 +36,12 @@ export const ml = (kibana: any) => { description: i18n.translate('xpack.ml.mlNavDescription', { defaultMessage: 'Machine Learning for the Elastic Stack', }), - icon: 'plugins/ml/ml.svg', + icon: 'plugins/ml/application/ml.svg', euiIconType: 'machineLearningApp', - main: 'plugins/ml/app', + main: 'plugins/ml/application/app', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'], + hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { isNamespaceAgnostic: true, @@ -79,7 +79,6 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, - usage: server.usage, }; const plugins = { @@ -87,6 +86,7 @@ export const ml = (kibana: any) => { security: server.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, + usageCollection: kbnServer.newPlatform.setup.plugins.usageCollection, ml: this, }; diff --git a/x-pack/legacy/plugins/ml/public/app.js b/x-pack/legacy/plugins/ml/public/app.js deleted file mode 100644 index b88346035f306..0000000000000 --- a/x-pack/legacy/plugins/ml/public/app.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -// import the uiExports that we want to "use" -import 'uiExports/fieldFormats'; -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; - -// needed to make syntax highlighting work in ace editors -import 'ace'; - -import 'plugins/ml/access_denied'; -import 'plugins/ml/jobs'; -import 'plugins/ml/overview'; -import 'plugins/ml/services/calendar_service'; -import 'plugins/ml/data_frame_analytics'; -import 'plugins/ml/datavisualizer'; -import 'plugins/ml/explorer'; -import 'plugins/ml/timeseriesexplorer'; -import 'plugins/ml/components/navigation_menu'; -import 'plugins/ml/components/loading_indicator'; -import 'plugins/ml/settings'; - -import uiRoutes from 'ui/routes'; - -if (typeof uiRoutes.enable === 'function') { - uiRoutes.enable(); -} - -uiRoutes - .otherwise({ - redirectTo: '/overview' - }); diff --git a/x-pack/legacy/plugins/ml/public/_app.scss b/x-pack/legacy/plugins/ml/public/application/_app.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_app.scss rename to x-pack/legacy/plugins/ml/public/application/_app.scss diff --git a/x-pack/legacy/plugins/ml/public/_hacks.scss b/x-pack/legacy/plugins/ml/public/application/_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_hacks.scss rename to x-pack/legacy/plugins/ml/public/application/_hacks.scss diff --git a/x-pack/legacy/plugins/ml/public/_variables.scss b/x-pack/legacy/plugins/ml/public/application/_variables.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_variables.scss rename to x-pack/legacy/plugins/ml/public/application/_variables.scss diff --git a/x-pack/legacy/plugins/ml/public/access_denied/index.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/access_denied/index.tsx rename to x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/access_denied/page.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/access_denied/page.tsx rename to x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/app.js b/x-pack/legacy/plugins/ml/public/application/app.js new file mode 100644 index 0000000000000..722e2c8d05e9b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/app.js @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import 'uiExports/savedObjectTypes'; + +import 'ui/autoload/all'; + +// needed to make syntax highlighting work in ace editors +import 'ace'; + +import './access_denied'; +import './jobs'; +import './overview'; +import './services/calendar_service'; +import './data_frame_analytics'; +import './datavisualizer'; +import './explorer'; +import './timeseriesexplorer'; +import './components/navigation_menu'; +import './components/loading_indicator'; +import './settings'; + +import uiRoutes from 'ui/routes'; + +if (typeof uiRoutes.enable === 'function') { + uiRoutes.enable(); +} + +uiRoutes + .otherwise({ + redirectTo: '/overview' + }); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx new file mode 100644 index 0000000000000..3d98e2d66935c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * React component for listing pairs of information about the detector for which + * rules are being edited. + */ + +import React from 'react'; + +import { EuiDescriptionList } from '@elastic/eui'; + +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { Annotation } from '../../../../../common/types/annotations'; +import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; + +interface Props { + annotation: Annotation; + intl: InjectedIntl; +} + +export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props) => { + const listItems = [ + { + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', + defaultMessage: 'Job ID', + }), + description: annotation.job_id, + }, + { + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', + defaultMessage: 'Start', + }), + description: formatHumanReadableDateTimeSeconds(annotation.timestamp), + }, + ]; + + if (annotation.end_timestamp !== undefined) { + listItems.push({ + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', + defaultMessage: 'End', + }), + description: formatHumanReadableDateTimeSeconds(annotation.end_timestamp), + }); + } + + if (annotation.create_time !== undefined && annotation.modified_time !== undefined) { + listItems.push({ + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle', + defaultMessage: 'Created', + }), + description: formatHumanReadableDateTimeSeconds(annotation.create_time), + }); + listItems.push({ + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdByTitle', + defaultMessage: 'Created by', + }), + description: annotation.create_username, + }); + listItems.push({ + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.lastModifiedTitle', + defaultMessage: 'Last modified', + }), + description: formatHumanReadableDateTimeSeconds(annotation.modified_time), + }); + listItems.push({ + title: intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle', + defaultMessage: 'Modified by', + }), + description: annotation.modified_username, + }); + } + + return ( + + ); +}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx new file mode 100644 index 0000000000000..7fa47f3518b81 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { injectObservablesAsProps } from '../../../util/observable_utils'; +import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; + +import React, { ComponentType } from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; + +import { Annotation } from '../../../../../common/types/annotations'; +import { annotation$ } from '../../../services/annotations_service'; + +import { AnnotationFlyout } from './index'; + +describe('AnnotationFlyout', () => { + test('Initialization.', () => { + const wrapper = shallowWithIntl(); + expect(wrapper).toMatchSnapshot(); + }); + + test('Update button is disabled with empty annotation', () => { + const annotation = mockAnnotations[1] as Annotation; + annotation$.next(annotation); + + // injectObservablesAsProps wraps the observable in a new component + const ObservableComponent = injectObservablesAsProps( + { annotation: annotation$ }, + (AnnotationFlyout as any) as ComponentType + ); + + const wrapper = mountWithIntl(); + const updateBtn = wrapper.find('EuiButton').first(); + expect(updateBtn.prop('isDisabled')).toEqual(true); + }); + + test('Error displayed and update button displayed if annotation text is longer than max chars', () => { + const annotation = mockAnnotations[2] as Annotation; + annotation$.next(annotation); + + // injectObservablesAsProps wraps the observable in a new component + const ObservableComponent = injectObservablesAsProps( + { annotation: annotation$ }, + (AnnotationFlyout as any) as ComponentType + ); + + const wrapper = mountWithIntl(); + const updateBtn = wrapper.find('EuiButton').first(); + expect(updateBtn.prop('isDisabled')).toEqual(true); + + expect(wrapper.find('EuiFormErrorText')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx new file mode 100644 index 0000000000000..84c16360795ea --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, ComponentType, Fragment, ReactNode } from 'react'; +import * as Rx from 'rxjs'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFormRow, + EuiSpacer, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; + +import { CommonProps } from '@elastic/eui'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { InjectedIntlProps } from 'react-intl'; +import { toastNotifications } from 'ui/notify'; +import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../../common/constants/annotations'; +import { + annotation$, + annotationsRefresh$, + AnnotationState, +} from '../../../services/annotations_service'; +import { injectObservablesAsProps } from '../../../util/observable_utils'; +import { AnnotationDescriptionList } from '../annotation_description_list'; +import { DeleteAnnotationModal } from '../delete_annotation_modal'; + +import { ml } from '../../../services/ml_api_service'; + +interface Props { + annotation: AnnotationState; +} + +interface State { + isDeleteModalVisible: boolean; +} + +class AnnotationFlyoutIntl extends Component { + public state: State = { + isDeleteModalVisible: false, + }; + + public annotationSub: Rx.Subscription | null = null; + + public annotationTextChangeHandler = (e: React.ChangeEvent) => { + if (this.props.annotation === null) { + return; + } + + annotation$.next({ + ...this.props.annotation, + annotation: e.target.value, + }); + }; + + public cancelEditingHandler = () => { + annotation$.next(null); + }; + + public deleteConfirmHandler = () => { + this.setState({ isDeleteModalVisible: true }); + }; + + public deleteHandler = async () => { + const { annotation, intl } = this.props; + + if (annotation === null) { + return; + } + + try { + await ml.annotations.deleteAnnotation(annotation._id); + toastNotifications.addSuccess( + intl.formatMessage( + { + id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.deletedAnnotationNotificationMessage', + defaultMessage: 'Deleted annotation for job with ID {jobId}.', + }, + { jobId: annotation.job_id } + ) + ); + } catch (err) { + toastNotifications.addDanger( + intl.formatMessage( + { + id: + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithDeletingAnnotationNotificationErrorMessage', + defaultMessage: + 'An error occurred deleting the annotation for job with ID {jobId}: {error}', + }, + { jobId: annotation.job_id, error: JSON.stringify(err) } + ) + ); + } + + this.closeDeleteModal(); + annotation$.next(null); + annotationsRefresh$.next(true); + }; + + public closeDeleteModal = () => { + this.setState({ isDeleteModalVisible: false }); + }; + + public validateAnnotationText = () => { + // Validates the entered text, returning an array of error messages + // for display in the form. An empty array is returned if the text is valid. + const { annotation, intl } = this.props; + const errors: string[] = []; + if (annotation === null) { + return errors; + } + + if (annotation.annotation.trim().length === 0) { + errors.push( + intl.formatMessage({ + id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError', + defaultMessage: 'Enter annotation text', + }) + ); + } + + const textLength = annotation.annotation.length; + if (textLength > ANNOTATION_MAX_LENGTH_CHARS) { + const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS; + errors.push( + intl.formatMessage( + { + id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError', + defaultMessage: + '{charsOver, number} {charsOver, plural, one {character} other {characters}} above maximum length of {maxChars}', + }, + { + maxChars: ANNOTATION_MAX_LENGTH_CHARS, + charsOver, + } + ) + ); + } + + return errors; + }; + + public saveOrUpdateAnnotation = () => { + const { annotation, intl } = this.props; + + if (annotation === null) { + return; + } + + annotation$.next(null); + + ml.annotations + .indexAnnotation(annotation) + .then(() => { + annotationsRefresh$.next(true); + if (typeof annotation._id === 'undefined') { + toastNotifications.addSuccess( + intl.formatMessage( + { + id: + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.addedAnnotationNotificationMessage', + defaultMessage: 'Added an annotation for job with ID {jobId}.', + }, + { jobId: annotation.job_id } + ) + ); + } else { + toastNotifications.addSuccess( + intl.formatMessage( + { + id: + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.updatedAnnotationNotificationMessage', + defaultMessage: 'Updated annotation for job with ID {jobId}.', + }, + { jobId: annotation.job_id } + ) + ); + } + }) + .catch(resp => { + if (typeof annotation._id === 'undefined') { + toastNotifications.addDanger( + intl.formatMessage( + { + id: + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithCreatingAnnotationNotificationErrorMessage', + defaultMessage: + 'An error occurred creating the annotation for job with ID {jobId}: {error}', + }, + { jobId: annotation.job_id, error: JSON.stringify(resp) } + ) + ); + } else { + toastNotifications.addDanger( + intl.formatMessage( + { + id: + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithUpdatingAnnotationNotificationErrorMessage', + defaultMessage: + 'An error occurred updating the annotation for job with ID {jobId}: {error}', + }, + { jobId: annotation.job_id, error: JSON.stringify(resp) } + ) + ); + } + }); + }; + + public render(): ReactNode { + const { annotation, intl } = this.props; + const { isDeleteModalVisible } = this.state; + + if (annotation === null) { + return null; + } + + const isExistingAnnotation = typeof annotation._id !== 'undefined'; + + // Check the length of the text is within the max length limit, + // and warn if the length is approaching the limit. + const validationErrors = this.validateAnnotationText(); + const isInvalid = validationErrors.length > 0; + const lengthRatioToShowWarning = 0.95; + let helpText = null; + if ( + isInvalid === false && + annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning + ) { + helpText = intl.formatMessage( + { + id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning', + defaultMessage: + '{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining', + }, + { charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length } + ); + } + + return ( + + + + +

+ {isExistingAnnotation ? ( + + ) : ( + + )} +

+
+
+ + + + + } + fullWidth + helpText={helpText} + isInvalid={isInvalid} + error={validationErrors} + > + + + + + + + + + + + + {isExistingAnnotation && ( + + + + )} + + + + {isExistingAnnotation ? ( + + ) : ( + + )} + + + + +
+ +
+ ); + } +} + +export const AnnotationFlyout = injectObservablesAsProps( + { annotation: annotation$ }, + (injectI18n(AnnotationFlyoutIntl) as any) as ComponentType +); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__mocks__/mock_annotations.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__mocks__/mock_annotations.json diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index ecd1c43eeb9ab..909abfd4abc23 100644 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -41,8 +41,8 @@ import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { mlTableService } from '../../../services/table_service'; -import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; -import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../common/util/job_utils'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; +import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../common/util/job_utils'; import { annotation$, annotationsRefresh$ } from '../../../services/annotations_service'; @@ -87,7 +87,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component { earliestMs: null, latestMs: null, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { this.setState((prevState, props) => ({ annotations: resp.annotations[props.jobs[0].job_id] || [], errorMessage: undefined, diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js similarity index 79% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 32d30741f43da..c3ca28dc96bfc 100644 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import jobConfig from '../../../../common/types/__mocks__/job_config_farequote'; +import jobConfig from '../../../../../common/types/__mocks__/job_config_farequote'; import mockAnnotations from './__mocks__/mock_annotations.json'; import './annotations_table.test.mocks'; @@ -24,13 +24,17 @@ jest.mock('../../../services/job_service', () => ({ } })); -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - annotations: { - getAnnotations: jest.fn().mockResolvedValue({ annotations: [] }) +jest.mock('../../../services/ml_api_service', () => { + const { of } = require('rxjs'); + const mockAnnotations$ = of({ annotations: [] }); + return { + ml: { + annotations: { + getAnnotations: jest.fn().mockReturnValue(mockAnnotations$) + } } - } -})); + };} +); describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts similarity index 81% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts index fd3b04b2332d8..4a29fec03da85 100644 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.mocks.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { chromeServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { chromeServiceMock } from '../../../../../../../../../src/core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx new file mode 100644 index 0000000000000..384ba67a62f8b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 PropTypes from 'prop-types'; +import React, { Fragment } from 'react'; + +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + cancelAction: () => void; + deleteAction: () => void; + isVisible: boolean; +} + +export const DeleteAnnotationModal: React.FC = ({ + cancelAction, + deleteAction, + isVisible, +}) => { + return ( + + {isVisible === true && ( + + + } + onCancel={cancelAction} + onConfirm={deleteAction} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + className="eui-textBreakWord" + /> + + )} + + ); +}; + +DeleteAnnotationModal.propTypes = { + cancelAction: PropTypes.func.isRequired, + deleteAction: PropTypes.func.isRequired, + isVisible: PropTypes.bool.isRequired, +}; diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/_anomalies_table.scss b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_anomalies_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/_anomalies_table.scss rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_anomalies_table.scss diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index dda516c819bf4..f3913879ff12c 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -30,7 +30,7 @@ import { getColumns } from './anomalies_table_columns'; import { AnomalyDetails } from './anomaly_details'; import { mlTableService } from '../../services/table_service'; -import { RuleEditorFlyout } from '../../components/rule_editor'; +import { RuleEditorFlyout } from '../rule_editor'; import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index be2a3e1f4223b..3e5d1e7acc450 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -30,7 +30,7 @@ import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; import { checkPermission } from '../../privilege/check_privilege'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { isRuleSupported } from '../../../common/util/anomaly_utils'; +import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; import { INFLUENCERS_LIMIT, diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_constants.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_constants.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_constants.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_constants.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index b1eada31fbf5b..bbbf7f704c614 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -35,8 +35,8 @@ import { getSeverity, showActualForFunction, showTypicalForFunction, -} from '../../../common/util/anomaly_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +} from '../../../../common/util/anomaly_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; import { formatValue } from '../../formatters/format_value'; import { MAX_CHARS } from './anomalies_table_constants'; diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.test.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/description_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/description_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/description_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/description_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/detector_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/detector_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/detector_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/detector_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/influencers_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/influencers_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/influencers_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/influencers_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/links_menu.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index dfa12e34928d2..19cd77655f97c 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -22,11 +22,11 @@ import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public'; import { checkPermission } from '../../privilege/check_privilege'; -import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; -import { isRuleSupported } from '../../../common/util/anomaly_utils'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; +import { isRuleSupported } from '../../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; import { escapeDoubleQuotes } from '../kql_filter_bar/utils'; import { getFieldTypeFromMapping } from '../../services/mapping_service'; import { ml } from '../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/index.ts b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx similarity index 91% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.tsx rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx index e74d1a73b3332..2288106c6a8ed 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx @@ -6,8 +6,8 @@ import React, { FC, memo } from 'react'; import { EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../../common/constants/multi_bucket_impact'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; interface SeverityCellProps { /** diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.tsx b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.tsx rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index ea9bc4f0f92ee..42a3e97509452 100644 --- a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -10,7 +10,7 @@ import { TooltipValueFormatter } from '@elastic/charts'; // TODO: Below import is temporary, use `react-use` lib instead. // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useObservable } from '../../../../../../../src/plugins/kibana_react/public/util/use_observable'; +import { useObservable } from '../../../../../../../../src/plugins/kibana_react/public/util/use_observable'; import { chartTooltip$, ChartTooltipValue } from './chart_tooltip_service'; diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.d.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.js b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.js rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.test.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.test.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/controls/_controls.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/_controls.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/_controls.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/_controls.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.test.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.test.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/_select_severity.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_select_severity.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/_select_severity.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_select_severity.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js index 502d6078ffd3b..9e6cffa21c5fa 100644 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.js +++ b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js @@ -22,7 +22,7 @@ import { EuiText, } from '@elastic/eui'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; import { injectObservablesAsProps } from '../../../util/observable_utils'; const warningLabel = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', { defaultMessage: 'warning' }); diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.test.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.test.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/create_job_link_card.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx rename to x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/create_job_link_card.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts b/x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts b/x-pack/legacy/plugins/ml/public/application/components/custom_hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/custom_hooks/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts b/x-pack/legacy/plugins/ml/public/application/components/custom_hooks/use_partial_state.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts rename to x-pack/legacy/plugins/ml/public/application/components/custom_hooks/use_partial_state.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js index b303ed9b7f008..9c4baacd9eec7 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { RecognizedResult } from './recognized_result'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../services/ml_api_service'; export class DataRecognizer extends Component { constructor(props) { diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx b/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx rename to x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/index.ts b/x-pack/legacy/plugins/ml/public/application/components/display_value/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/display_value/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.scss b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.scss rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.scss diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.test.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.test.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/index.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/index.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/_field_title_bar.scss b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/_field_title_bar.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/_field_type_icon.scss b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/_field_type_icon.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js index 20c8e5310788d..c219aeb84772c 100644 --- a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.js +++ b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js @@ -12,7 +12,7 @@ import { EuiToolTip } from '@elastic/eui'; // don't use something like plugins/ml/../common // because it won't work with the jest tests import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { i18n } from '@kbn/i18n'; export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true }) => { diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js similarity index 95% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.test.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js index 11e98549684ee..c21a173db4a85 100644 --- a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js @@ -8,7 +8,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { FieldTypeIcon } from './field_type_icon'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; describe('FieldTypeIcon', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/_influencers_list.scss b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/_influencers_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/_influencers_list.scss rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/_influencers_list.scss diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/index.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/index.js rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/influencers_list.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/influencers_list.js rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js index 7794d3fd23497..6551996e2d194 100644 --- a/x-pack/legacy/plugins/ml/public/components/influencers_list/influencers_list.js +++ b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js @@ -22,8 +22,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { abbreviateWholeNumber } from 'plugins/ml/formatters/abbreviate_whole_number'; -import { getSeverity } from 'plugins/ml/../common/util/anomaly_utils'; +import { abbreviateWholeNumber } from '../../formatters/abbreviate_whole_number'; +import { getSeverity } from '../../../../common/util/anomaly_utils'; import { EntityCell } from '../entity_cell'; diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/items_grid/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/_items_grid.scss b/x-pack/legacy/plugins/ml/public/application/components/items_grid/_items_grid.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/_items_grid.scss rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/_items_grid.scss diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/index.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/index.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/items_grid.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/items_grid.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid.js diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/items_grid_pagination.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid_pagination.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/items_grid_pagination.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid_pagination.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/job_message_icon/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx similarity index 93% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx rename to x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx index 545e9231699fd..7546121250013 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import { AuditMessageBase } from '../../../common/types/audit_message'; +import { AuditMessageBase } from '../../../../common/types/audit_message'; interface Props { message: AuditMessageBase; diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts b/x-pack/legacy/plugins/ml/public/application/components/job_messages/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_messages/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/job_messages/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx similarity index 96% rename from x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx rename to x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx index 08f9a4379559b..aedb8b6d17d06 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -12,7 +12,7 @@ import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { JobMessage } from '../../../common/types/audit_message'; +import { JobMessage } from '../../../../common/types/audit_message'; import { JobIcon } from '../job_message_icon'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/job_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/_job_selector.scss b/x-pack/legacy/plugins/ml/public/application/components/job_selector/_job_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/_job_selector.scss rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/_job_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/custom_selection_table.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/custom_selection_table.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_select_service_utils.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_select_service_utils.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js index 13899be860428..7725cf5e59482 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js @@ -10,7 +10,7 @@ import { PropTypes } from 'prop-types'; import moment from 'moment'; import { ml } from '../../services/ml_api_service'; -import { JobSelectorTable } from './job_selector_table/'; +import { JobSelectorTable } from './job_selector_table'; import { IdBadges } from './id_badges'; import { NewSelectionIdBadges } from './new_selection_id_badges'; import { timefilter } from 'ui/timefilter'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js similarity index 94% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/job_selector_badge.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js index 2cb8a9da3dfdf..97f46c7cb59ea 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/job_selector_badge.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js @@ -9,7 +9,7 @@ import React from 'react'; import { PropTypes } from 'prop-types'; import { EuiBadge } from '@elastic/eui'; -import { tabColor } from '../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../common/util/group_color_utils'; import { i18n } from '@kbn/i18n'; export function JobSelectorBadge({ diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index a754fbfab5ca6..76417984828d2 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -10,7 +10,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; import { CustomSelectionTable } from '../custom_selection_table'; import { JobSelectorBadge } from '../job_selector_badge'; -import { TimeRangeBar } from '../timerange_bar/'; +import { TimeRangeBar } from '../timerange_bar'; import { EuiFlexGroup, diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__tests__/utils.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js new file mode 100644 index 0000000000000..18b3382175fdd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { npStart } from 'ui/new_platform'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; + +const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); + +export async function getSuggestions( + query, + selectionStart, + indexPattern, + boolFilter +) { + const autocompleteProvider = getAutocompleteProvider('kuery'); + if (!autocompleteProvider) { + return []; + } + const config = { + get: () => true + }; + + const getAutocompleteSuggestions = autocompleteProvider({ + config, + indexPatterns: [indexPattern], + boolFilter + }); + return getAutocompleteSuggestions({ + query, + selectionStart, + selectionEnd: selectionStart + }); +} + +function convertKueryToEsQuery(kuery, indexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); +} +// Recommended by MDN for escaping user input to be treated as a literal string within a regular expression +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +export function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +export function escapeParens(string) { + return string.replace(/[()]/g, '\\$&'); +} + +export function escapeDoubleQuotes(string) { + return string.replace(/\"/g, '\\$&'); +} + +export function getKqlQueryValues(inputValue, indexPattern) { + const ast = esKuery.fromKueryExpression(inputValue); + const isAndOperator = (ast.function === 'and'); + const query = convertKueryToEsQuery(inputValue, indexPattern); + const filteredFields = []; + + if (!query) { + return; + } + + // if ast.type == 'function' then layout of ast.arguments: + // [{ arguments: [ { type: 'literal', value: 'AAL' } ] },{ arguments: [ { type: 'literal', value: 'AAL' } ] }] + if (ast && Array.isArray(ast.arguments)) { + + ast.arguments.forEach((arg) => { + if (arg.arguments !== undefined) { + arg.arguments.forEach((nestedArg) => { + if (typeof nestedArg.value === 'string') { + filteredFields.push(nestedArg.value); + } + }); + } else if (typeof arg.value === 'string') { + filteredFields.push(arg.value); + } + }); + + } + + return { + filterQuery: query, + filteredFields, + queryString: inputValue, + isAndOperator, + tableQueryString: inputValue + }; +} + +export function getQueryPattern(fieldName, fieldValue) { + const sanitizedFieldName = escapeRegExp(fieldName); + const sanitizedFieldValue = escapeRegExp(fieldValue); + + return new RegExp(`(${sanitizedFieldName})\\s?:\\s?(")?(${sanitizedFieldValue})(")?`, 'i'); +} + +export function removeFilterFromQueryString(currentQueryString, fieldName, fieldValue) { + let newQueryString = ''; + // Remove the passed in fieldName and value from the existing filter + const queryPattern = getQueryPattern(fieldName, fieldValue); + newQueryString = currentQueryString.replace(queryPattern, ''); + // match 'and' or 'or' at the start/end of the string + const endPattern = /\s(and|or)\s*$/ig; + const startPattern = /^\s*(and|or)\s/ig; + // If string has a double operator (e.g. tag:thing or or tag:other) remove and replace with the first occurring operator + const invalidOperatorPattern = /\s+(and|or)\s+(and|or)\s+/ig; + newQueryString = newQueryString.replace(invalidOperatorPattern, ' $1 '); + // If string starts/ends with 'and' or 'or' remove that as that is illegal kuery syntax + newQueryString = newQueryString.replace(endPattern, ''); + newQueryString = newQueryString.replace(startPattern, ''); + + return newQueryString; +} diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/__tests__/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/__tests__/loading_indicator_directive.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/_loading_indicator.scss b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/_loading_indicator.scss rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/index.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/index.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.html b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.html rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_wrapper.html b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_wrapper.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_wrapper.html rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_wrapper.html diff --git a/x-pack/legacy/plugins/ml/public/components/message_call_out/index.js b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/message_call_out/index.js rename to x-pack/legacy/plugins/ml/public/application/components/message_call_out/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/message_call_out/message_call_out.js b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js similarity index 94% rename from x-pack/legacy/plugins/ml/public/components/message_call_out/message_call_out.js rename to x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js index 830510df25659..d26fc296e9744 100644 --- a/x-pack/legacy/plugins/ml/public/components/message_call_out/message_call_out.js +++ b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js @@ -16,7 +16,7 @@ import { EuiCallOut } from '@elastic/eui'; // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { MESSAGE_LEVEL } from '../../../common/constants/message_levels'; +import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; function getCallOutAttributes(message, status) { diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/index.ts b/x-pack/legacy/plugins/ml/public/application/components/messagebar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts new file mode 100644 index 0000000000000..bbd793696e005 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ProgressBar, mlInMemoryTableFactory } from './ml_in_memory_table'; +export * from './types'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx new file mode 100644 index 0000000000000..7caaadf65d6da --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This component extends EuiInMemoryTable with some +// fixes and TS specs until the changes become available upstream. + +import React, { Fragment } from 'react'; + +import { EuiProgress } from '@elastic/eui'; + +// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement +// of the table and doesn't play well with auto-refreshing. That's why we're displaying +// our own progress bar on top of the table. `EuiProgress` after `isLoading` displays +// the loading indicator. The variation after `!isLoading` displays an empty progress +// bar fixed to 0%. Without it, the display would vertically jump when showing/hiding +// the progress bar. +export const ProgressBar = ({ isLoading = false }) => { + return ( + + {isLoading && } + {!isLoading && ( + + )} + + ); +}; + +// copied from EUI to be available to the extended getDerivedStateFromProps() +function findColumnByProp(columns: any, prop: any, value: any) { + for (let i = 0; i < columns.length; i++) { + const column = columns[i]; + if (column[prop] === value) { + return column; + } + } +} + +// copied from EUI to be available to the extended getDerivedStateFromProps() +const getInitialSorting = (columns: any, sorting: any) => { + if (!sorting || !sorting.sort) { + return { + sortName: undefined, + sortDirection: undefined, + }; + } + + const { field: sortable, direction: sortDirection } = sorting.sort; + + // sortable could be a column's `field` or its `name` + // for backwards compatibility `field` must be checked first + let sortColumn = findColumnByProp(columns, 'field', sortable); + if (sortColumn == null) { + sortColumn = findColumnByProp(columns, 'name', sortable); + } + + if (sortColumn == null) { + return { + sortName: undefined, + sortDirection: undefined, + }; + } + + const sortName = sortColumn.name; + + return { + sortName, + sortDirection, + }; +}; + +import { mlInMemoryTableBasicFactory } from './types'; + +export function mlInMemoryTableFactory() { + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + + return class MlInMemoryTable extends MlInMemoryTableBasic { + static getDerivedStateFromProps(nextProps: any, prevState: any) { + const derivedState = { + ...prevState.prevProps, + pageIndex: nextProps.pagination.initialPageIndex, + pageSize: nextProps.pagination.initialPageSize, + }; + + if (nextProps.items !== prevState.prevProps.items) { + Object.assign(derivedState, { + prevProps: { + items: nextProps.items, + }, + }); + } + + const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); + if ( + sortName !== prevState.prevProps.sortName || + sortDirection !== prevState.prevProps.sortDirection + ) { + Object.assign(derivedState, { + sortName, + sortDirection, + }); + } + return derivedState; + } + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts new file mode 100644 index 0000000000000..49d831de47387 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Component, HTMLAttributes, ReactElement, ReactNode } from 'react'; + +import { CommonProps, EuiInMemoryTable } from '@elastic/eui'; + +// Not using an enum here because the original HorizontalAlignment is also a union type of string. +type HorizontalAlignment = 'left' | 'center' | 'right'; + +type SortableFunc = (item: T) => any; +type Sortable = boolean | SortableFunc; +type DATA_TYPES = any; +type FooterFunc = (payload: { items: T[]; pagination: any }) => ReactNode; +type RenderFunc = (value: any, record?: any) => ReactNode; +export interface FieldDataColumnType { + field: string; + name: ReactNode; + description?: string; + dataType?: DATA_TYPES; + width?: string; + sortable?: Sortable; + align?: HorizontalAlignment; + truncateText?: boolean; + render?: RenderFunc; + footer?: string | ReactElement | FooterFunc; + textOnly?: boolean; + 'data-test-subj'?: string; +} + +export interface ComputedColumnType { + render: RenderFunc; + name?: ReactNode; + description?: string; + sortable?: (item: T) => any; + width?: string; + truncateText?: boolean; + 'data-test-subj'?: string; +} + +type ICON_TYPES = any; +type IconTypesFunc = (item: T) => ICON_TYPES; // (item) => oneOf(ICON_TYPES) +type BUTTON_ICON_COLORS = any; +type ButtonIconColorsFunc = (item: T) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS) +interface DefaultItemActionType { + type?: 'icon' | 'button'; + name: string; + description: string; + onClick?(item: T): void; + href?: string; + target?: string; + available?(item: T): boolean; + enabled?(item: T): boolean; + isPrimary?: boolean; + icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon' + color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc; +} + +interface CustomItemActionType { + render(item: T, enabled: boolean): ReactNode; + available?(item: T): boolean; + enabled?(item: T): boolean; + isPrimary?: boolean; +} + +export interface ExpanderColumnType { + align?: HorizontalAlignment; + width?: string; + isExpander: boolean; + render: RenderFunc; +} + +type SupportedItemActionType = DefaultItemActionType | CustomItemActionType; + +export interface ActionsColumnType { + actions: Array>; + name?: ReactNode; + description?: string; + width?: string; +} + +export type ColumnType = + | ActionsColumnType + | ComputedColumnType + | ExpanderColumnType + | FieldDataColumnType; + +type QueryType = any; + +interface Schema { + strict?: boolean; + fields?: Record; + flags?: string[]; +} + +interface SearchBoxConfigPropTypes { + placeholder?: string; + incremental?: boolean; + schema?: Schema; +} + +interface Box { + placeholder?: string; + incremental?: boolean; + // here we enable the user to just assign 'true' to the schema, in which case + // we will auto-generate it out of the columns configuration + schema?: boolean | SearchBoxConfigPropTypes['schema']; +} + +type SearchFiltersFiltersType = any; + +interface ExecuteQueryOptions { + defaultFields: string[]; + isClauseMatcher: () => void; + explain: boolean; +} + +type SearchType = + | boolean + | { + toolsLeft?: ReactNode; + toolsRight?: ReactNode; + defaultQuery?: QueryType; + box?: Box; + filters?: SearchFiltersFiltersType; + onChange?: (arg: any) => void; + executeQueryOptions?: ExecuteQueryOptions; + }; + +interface PageSizeOptions { + pageSizeOptions: number[]; +} +interface InitialPageOptions extends PageSizeOptions { + initialPageIndex: number; + initialPageSize: number; +} +type PaginationProp = boolean | PageSizeOptions | InitialPageOptions; + +export enum SORT_DIRECTION { + ASC = 'asc', + DESC = 'desc', +} +export type SortDirection = SORT_DIRECTION.ASC | SORT_DIRECTION.DESC; +export interface Sorting { + sort: { + field: string; + direction: SortDirection; + }; +} +export type SortingPropType = boolean | Sorting; + +type SelectionType = any; + +export interface OnTableChangeArg extends Sorting { + page: { index: number; size: number }; +} + +type ItemIdTypeFunc = (item: T) => string; +type ItemIdType = + | string // the name of the item id property + | ItemIdTypeFunc; + +export type EuiInMemoryTableProps = CommonProps & { + columns: Array>; + hasActions?: boolean; + isExpandable?: boolean; + isSelectable?: boolean; + items?: T[]; + loading?: boolean; + message?: HTMLAttributes; + error?: string; + compressed?: boolean; + search?: SearchType; + pagination?: PaginationProp; + sorting?: SortingPropType; + // Set `allowNeutralSort` to false to force column sorting. Defaults to true. + allowNeutralSort?: boolean; + responsive?: boolean; + selection?: SelectionType; + itemId?: ItemIdType; + itemIdToExpandedRowMap?: Record; + rowProps?: (item: T) => void | Record; + cellProps?: () => void | Record; + onTableChange?: (arg: OnTableChangeArg) => void; +}; + +type EuiInMemoryTableType = typeof EuiInMemoryTable; + +interface ComponentWithConstructor extends EuiInMemoryTableType { + new (): Component; +} + +export function mlInMemoryTableBasicFactory() { + return EuiInMemoryTable as ComponentWithConstructor>; +} diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/_navigation_menu.scss b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/_navigation_menu.scss rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/main_tabs.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap index b898558b20738..f9df085d2cbe7 100644 --- a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap @@ -69,6 +69,7 @@ exports[`Navigation Menu: Minimal initialization. 1`] = ` refreshInterval={0} showUpdateButton={true} start="Thu Aug 29 2019 02:04:19 GMT+0200" + timeFormat="HH:mm" />
diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/index.ts b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/node_available_warning/index.ts b/x-pack/legacy/plugins/ml/public/application/components/node_available_warning/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/node_available_warning/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/node_available_warning/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/node_available_warning/node_available_warning.tsx b/x-pack/legacy/plugins/ml/public/application/components/node_available_warning/node_available_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/node_available_warning/node_available_warning.tsx rename to x-pack/legacy/plugins/ml/public/application/components/node_available_warning/node_available_warning.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js new file mode 100644 index 0000000000000..77d2ed4643fca --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import expect from '@kbn/expect'; +import { + isValidRule, + buildRuleDescription, + getAppliesToValueFromAnomaly, +} from '../utils'; +import { + ACTION, + APPLIES_TO, + OPERATOR, + FILTER_TYPE, +} from '../../../../../common/constants/detector_rule'; + +describe('ML - rule editor utils', () => { + + const ruleWithCondition = { + actions: [ACTION.SKIP_RESULT], + conditions: [ + { + applies_to: APPLIES_TO.ACTUAL, + operator: OPERATOR.GREATER_THAN, + value: 10 + } + ] + }; + + const ruleWithScope = { + actions: [ACTION.SKIP_RESULT], + scope: { + instance: { + filter_id: 'test_aws_instances', + filter_type: FILTER_TYPE.INCLUDE, + enabled: true + } + } + }; + + const ruleWithConditionAndScope = { + actions: [ACTION.SKIP_RESULT], + conditions: [ + { + applies_to: APPLIES_TO.TYPICAL, + operator: OPERATOR.LESS_THAN, + value: 100 + } + ], + scope: { + instance: { + filter_id: 'test_aws_instances', + filter_type: FILTER_TYPE.EXCLUDE, + enabled: true + } + } + }; + + describe('isValidRule', () => { + + it('returns true for a rule with an action and a condition', () => { + expect(isValidRule(ruleWithCondition)).to.be(true); + }); + + it('returns true for a rule with an action and scope', () => { + expect(isValidRule(ruleWithScope)).to.be(true); + }); + + it('returns true for a rule with an action, scope and condition', () => { + expect(isValidRule(ruleWithConditionAndScope)).to.be(true); + }); + + it('returns false for a rule with no action', () => { + const ruleWithNoAction = { + actions: [], + conditions: [ + { + applies_to: APPLIES_TO.TYPICAL, + operator: OPERATOR.LESS_THAN, + value: 100 + } + ], + }; + + expect(isValidRule(ruleWithNoAction)).to.be(false); + }); + + it('returns false for a rule with no scope or conditions', () => { + const ruleWithNoScopeOrCondition = { + actions: [ACTION.SKIP_RESULT], + }; + + expect(isValidRule(ruleWithNoScopeOrCondition)).to.be(false); + }); + + }); + + describe('buildRuleDescription', () => { + + it('returns expected rule descriptions', () => { + expect(buildRuleDescription(ruleWithCondition)).to.be( + 'skip result when actual is greater than 10'); + expect(buildRuleDescription(ruleWithScope)).to.be( + 'skip result when instance is in test_aws_instances'); + expect(buildRuleDescription(ruleWithConditionAndScope)).to.be( + 'skip result when typical is less than 100 AND instance is not in test_aws_instances'); + }); + }); + + describe('getAppliesToValueFromAnomaly', () => { + + const anomaly = { + actual: [210], + typical: [1.23], + }; + + it('returns expected actual value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).to.be(210); + }); + + it('returns expected typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).to.be(1.23); + }); + + it('returns expected diff from typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).to.be(208.77); + }); + }); + +}); diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/_rule_editor.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/_rule_editor.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/_rule_editor.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/_rule_editor.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js index 9ed6b2220f196..f5340c9fc2374 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; -import { ACTION } from '../../../common/constants/detector_rule'; +import { ACTION } from '../../../../common/constants/detector_rule'; import { FormattedMessage } from '@kbn/i18n/react'; export function ActionsSection({ diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js similarity index 95% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js index 9ba85c5caddba..04e2c764e5ed9 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js @@ -9,7 +9,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ActionsSection } from './actions_section'; -import { ACTION } from '../../../common/constants/detector_rule'; +import { ACTION } from '../../../../common/constants/detector_rule'; describe('ActionsSection', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_detector_description_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_detector_description_list.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js index a69ed1d7a5583..aeadcb8bf58ad 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js @@ -25,7 +25,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; import { appliesToText, operatorToText } from './utils'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js similarity index 94% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js index 5dcbe845f1176..640f90744aa8e 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js @@ -11,7 +11,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ConditionExpression } from './condition_expression'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; describe('ConditionExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js index 8db05d4752e45..e2bb62fa03790 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js @@ -12,7 +12,7 @@ import React from 'react'; import { ConditionsSection } from './conditions_section'; import { getNewConditionDefaults } from './utils'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; describe('ConditionsSectionExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 904185fccf3f3..1ccfa5b664f59 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -47,8 +47,8 @@ import { addItemToFilter, } from './utils'; -import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../common/constants/detector_rule'; -import { getPartitioningFieldNames } from '../../../common/util/job_utils'; +import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../../common/constants/detector_rule'; +import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { metadata } from 'ui/metadata'; diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js index 18fdf8887dc7a..fe7bf81b6aca2 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js @@ -24,7 +24,7 @@ import { EuiSelect, } from '@elastic/eui'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; import { filterTypeToText } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js index 3acd46b5d6aef..68be030d7c28a 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js @@ -12,7 +12,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ScopeExpression } from './scope_expression'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; describe('ScopeExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js index f3a6c57e1cab4..b7c961758fbf2 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -20,7 +20,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ScopeSection } from './scope_section'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; describe('ScopeSection', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js index 0df0ded392be2..48dd62b436852 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js @@ -23,7 +23,7 @@ import { EuiText, } from '@elastic/eui'; -import { APPLIES_TO } from '../../../../common/constants/detector_rule'; +import { APPLIES_TO } from '../../../../../common/constants/detector_rule'; import { formatValue } from '../../../formatters/format_value'; import { getAppliesToValueFromAnomaly, diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js index 206acb65b4ba3..866f2ff40a6d4 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js @@ -10,7 +10,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { EditConditionLink } from './edit_condition_link'; -import { APPLIES_TO } from '../../../../common/constants/detector_rule'; +import { APPLIES_TO } from '../../../../../common/constants/detector_rule'; function prepareTest(updateConditionValueFn, appliesTo) { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js index d9fc6b9ed64cf..c0553c32eaf24 100644 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js @@ -32,7 +32,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { RuleActionPanel } from './rule_action_panel'; -import { ACTION } from '../../../../common/constants/detector_rule'; +import { ACTION } from '../../../../../common/constants/detector_rule'; describe('RuleActionPanel', () => { diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js new file mode 100644 index 0000000000000..a6e3950a75a01 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ACTION, APPLIES_TO, FILTER_TYPE, OPERATOR } from '../../../../common/constants/detector_rule'; + +import { cloneDeep } from 'lodash'; +import { ml } from '../../services/ml_api_service'; +import { mlJobService } from '../../services/job_service'; +import { i18n } from '@kbn/i18n'; +import { processCreatedBy } from '../../../../common/util/job_utils'; + +export function getNewConditionDefaults() { + return { + applies_to: APPLIES_TO.ACTUAL, + operator: OPERATOR.LESS_THAN, + value: 1 + }; +} + +export function getNewRuleDefaults() { + return { + actions: [ACTION.SKIP_RESULT], + conditions: [] + }; +} + +export function getScopeFieldDefaults(filterListIds) { + const defaults = { + filter_type: FILTER_TYPE.INCLUDE, + enabled: false, // UI-only property to show field as enabled in Scope section. + }; + + if (filterListIds !== undefined && filterListIds.length > 0) { + defaults.filter_id = filterListIds[0]; + } + + return defaults; +} + +export function isValidRule(rule) { + // Runs simple checks to make sure the minimum set of + // properties have values in the edited rule. + let isValid = false; + + // Check an action has been supplied. + const actions = rule.actions; + if (actions.length > 0) { + // Check either a condition or a scope property has been set. + const conditions = rule.conditions; + if (conditions !== undefined && conditions.length > 0) { + isValid = true; + } else { + const scope = rule.scope; + if (scope !== undefined) { + isValid = Object.keys(scope).some(field => (scope[field].enabled === true)); + } + } + } + + return isValid; +} + +export function saveJobRule(job, detectorIndex, ruleIndex, editedRule) { + const detector = job.analysis_config.detectors[detectorIndex]; + + // Filter out any scope expression where the UI=specific 'enabled' + // property is set to false. + const clonedRule = cloneDeep(editedRule); + const scope = clonedRule.scope; + if (scope !== undefined) { + Object.keys(scope).forEach((field) => { + if (scope[field].enabled === false) { + delete scope[field]; + } else { + // Remove the UI-only property as it is rejected by the endpoint. + delete scope[field].enabled; + } + }); + } + + let rules = []; + if (detector.custom_rules === undefined) { + rules = [clonedRule]; + } else { + rules = cloneDeep(detector.custom_rules); + + if (ruleIndex < rules.length) { + // Edit to an existing rule. + rules[ruleIndex] = clonedRule; + } else { + // Add a new rule. + rules.push(clonedRule); + } + } + + return updateJobRules(job, detectorIndex, rules); +} + +export function deleteJobRule(job, detectorIndex, ruleIndex) { + const detector = job.analysis_config.detectors[detectorIndex]; + let customRules = []; + if (detector.custom_rules !== undefined && ruleIndex < detector.custom_rules.length) { + customRules = cloneDeep(detector.custom_rules); + customRules.splice(ruleIndex, 1); + return updateJobRules(job, detectorIndex, customRules); + } else { + return Promise.reject(new Error( + i18n.translate('xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage', { + defaultMessage: 'Rule no longer exists for detector index {detectorIndex} in job {jobId}', + values: { + detectorIndex, + jobId: job.job_id + } + }) + )); + } +} + + +export function updateJobRules(job, detectorIndex, rules) { + // Pass just the detector with the edited rule to the updateJob endpoint. + const jobId = job.job_id; + const jobData = { + detectors: [ + { + detector_index: detectorIndex, + custom_rules: rules + } + ] + }; + + let customSettings = {}; + if (job.custom_settings !== undefined) { + customSettings = { ...job.custom_settings }; + processCreatedBy(customSettings); + jobData.custom_settings = customSettings; + } + + return new Promise((resolve, reject) => { + mlJobService.updateJob(jobId, jobData) + .then((resp) => { + if (resp.success) { + // Refresh the job data in the job service before resolving. + mlJobService.refreshJob(jobId) + .then(() => { + resolve({ success: true }); + }) + .catch((refreshResp) => { + reject(refreshResp); + }); + } else { + reject(resp); + } + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Updates an ML filter used in the scope part of a rule, +// adding an item to the filter with the specified ID. +export function addItemToFilter(item, filterId) { + return new Promise((resolve, reject) => { + ml.filters.updateFilter( + filterId, + undefined, + [item], + undefined + ) + .then((updatedFilter) => { + resolve(updatedFilter); + }) + .catch((error) => { + reject(error); + }); + }); +} + +export function buildRuleDescription(rule) { + const { actions, conditions, scope } = rule; + let actionsText = ''; + let conditionsText = ''; + let filtersText = ''; + + actions.forEach((action, i) => { + if (i > 0) { + actionsText += ' AND '; + } + switch (action) { + case ACTION.SKIP_RESULT: + actionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.resultActionTypeText', { + defaultMessage: 'result', + description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText +' + + 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' + }); + break; + case ACTION.SKIP_MODEL_UPDATE: + actionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.modelUpdateActionTypeText', { + defaultMessage: 'model update', + description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + + 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' + }); + break; + } + }); + + if (conditions !== undefined) { + conditions.forEach((condition, i) => { + if (i > 0) { + conditionsText += ' AND '; + } + conditionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.conditionsText', { + defaultMessage: '{appliesTo} is {operator} {value}', + values: { appliesTo: appliesToText(condition.applies_to), operator: operatorToText(condition.operator), value: condition.value }, + description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + + 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' + }); + }); + } + + if (scope !== undefined) { + if (conditions !== undefined && conditions.length > 0) { + filtersText += ' AND '; + } + const fieldNames = Object.keys(scope); + fieldNames.forEach((fieldName, i) => { + if (i > 0) { + filtersText += ' AND '; + } + + const filter = scope[fieldName]; + filtersText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.filtersText', { + defaultMessage: '{fieldName} is {filterType} {filterId}', + values: { fieldName, filterType: filterTypeToText(filter.filter_type), filterId: filter.filter_id }, + description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + + 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' + }); + }); + } + + return i18n.translate('xpack.ml.ruleEditor.ruleDescription', { + defaultMessage: 'skip {actions} when {conditions}{filters}', + values: { + actions: actionsText, + conditions: conditionsText, + filters: filtersText + }, + description: 'Composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + + 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText.' + + ' (Example: skip model update when actual is less than 1 AND ip is in xxx)' + }); +} + +export function filterTypeToText(filterType) { + switch (filterType) { + case FILTER_TYPE.INCLUDE: + return i18n.translate('xpack.ml.ruleEditor.includeFilterTypeText', { defaultMessage: 'in' }); + case FILTER_TYPE.EXCLUDE: + return i18n.translate('xpack.ml.ruleEditor.excludeFilterTypeText', { defaultMessage: 'not in' }); + + default: + return (filterType !== undefined) ? filterType : ''; + } +} + +export function appliesToText(appliesTo) { + switch (appliesTo) { + case APPLIES_TO.ACTUAL: + return i18n.translate('xpack.ml.ruleEditor.actualAppliesTypeText', { defaultMessage: 'actual' }); + case APPLIES_TO.TYPICAL: + return i18n.translate('xpack.ml.ruleEditor.typicalAppliesTypeText', { defaultMessage: 'typical' }); + + case APPLIES_TO.DIFF_FROM_TYPICAL: + return i18n.translate('xpack.ml.ruleEditor.diffFromTypicalAppliesTypeText', { defaultMessage: 'diff from typical' }); + + default: + return (appliesTo !== undefined) ? appliesTo : ''; + } +} + +export function operatorToText(operator) { + switch (operator) { + case OPERATOR.LESS_THAN: + return i18n.translate('xpack.ml.ruleEditor.lessThanOperatorTypeText', { defaultMessage: 'less than' }); + + case OPERATOR.LESS_THAN_OR_EQUAL: + return i18n.translate('xpack.ml.ruleEditor.lessThanOrEqualToOperatorTypeText', { defaultMessage: 'less than or equal to' }); + + case OPERATOR.GREATER_THAN: + return i18n.translate('xpack.ml.ruleEditor.greaterThanOperatorTypeText', { defaultMessage: 'greater than' }); + + case OPERATOR.GREATER_THAN_OR_EQUAL: + return i18n.translate('xpack.ml.ruleEditor.greaterThanOrEqualToOperatorTypeText', { defaultMessage: 'greater than or equal to' }); + + default: + return (operator !== undefined) ? operator : ''; + } +} + +// Returns the value of the selected 'applies_to' field from the +// selected anomaly i.e. the actual, typical or diff from typical. +export function getAppliesToValueFromAnomaly(anomaly, appliesTo) { + let actualValue; + let typicalValue; + + const actual = anomaly.actual; + if (actual !== undefined) { + actualValue = Array.isArray(actual) ? actual[0] : actual; + } + + const typical = anomaly.typical; + if (typical !== undefined) { + typicalValue = Array.isArray(typical) ? typical[0] : typical; + } + + switch (appliesTo) { + case APPLIES_TO.ACTUAL: + return actualValue; + + case APPLIES_TO.TYPICAL: + return typicalValue; + + case APPLIES_TO.DIFF_FROM_TYPICAL: + if (actual !== undefined && typical !== undefined) { + return Math.abs(actualValue - typicalValue); + } + } + + return undefined; +} diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_stat.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stat.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_stat.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stat.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_stats_bar.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stats_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_stats_bar.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stats_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/index.ts b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/stat.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/stat.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/stats_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/stats_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/upgrade/index.ts b/x-pack/legacy/plugins/ml/public/application/components/upgrade/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/upgrade/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/upgrade/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/upgrade/upgrade_warning.tsx b/x-pack/legacy/plugins/ml/public/application/components/upgrade/upgrade_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/upgrade/upgrade_warning.tsx rename to x-pack/legacy/plugins/ml/public/application/components/upgrade/upgrade_warning.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/__snapshots__/validate_job_view.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/__snapshots__/validate_job_view.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/index.ts b/x-pack/legacy/plugins/ml/public/application/components/validate_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.d.ts b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js index a400e37e85219..50dc2b7f43f99 100644 --- a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -37,8 +37,8 @@ const jobTipsUrl = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/jo // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { VALIDATION_STATUS } from '../../../common/constants/validation'; -import { getMostSevereMessageStatus } from '../../../common/util/validation_utils'; +import { VALIDATION_STATUS } from '../../../../common/constants/validation'; +import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; const defaultIconType = 'questionInCircle'; const getDefaultState = () => ({ diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.test.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.test.js rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_patterns.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_patterns.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_config.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_config.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_context_value.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_context_value.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts new file mode 100644 index 0000000000000..2bff760ed3711 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; +import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; + +export const savedSearchMock = { + id: 'the-saved-search-id', + title: 'the-saved-search-title', + searchSource: searchSourceMock as SearchSourceContract, + columns: [], + sort: [], + destroy: () => {}, +}; diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/index.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/index.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/ui_context.tsx b/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/ui_context.tsx rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss new file mode 100644 index 0000000000000..c231c405b5369 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -0,0 +1,5 @@ +@import 'pages/analytics_exploration/components/exploration/index'; +@import 'pages/analytics_exploration/components/regression_exploration/index'; +@import 'pages/analytics_management/components/analytics_list/index'; +@import 'pages/analytics_management/components/create_analytics_form/index'; +@import 'pages/analytics_management/components/create_analytics_flyout/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts new file mode 100644 index 0000000000000..fde854b7f41c3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +export function getDataFrameAnalyticsBreadcrumbs() { + return [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts similarity index 79% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 04dff6e0b4dc5..344a82f4d54d4 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -9,8 +9,11 @@ import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { idx } from '@kbn/elastic-idx'; +import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; +import { Dictionary } from '../../../../common/types/common'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; +import { SavedSearchQuery } from '../../contexts/kibana'; export type IndexName = string; export type IndexPattern = string; @@ -30,6 +33,16 @@ interface RegressionAnalysis { export const SEARCH_SIZE = 1000; +export const defaultSearchQuery = { + match_all: {}, +}; + +export interface SearchQuery { + track_total_hits?: boolean; + query: SavedSearchQuery; + sort?: any; +} + export enum INDEX_STATUS { UNUSED, LOADING, @@ -38,8 +51,8 @@ export enum INDEX_STATUS { } export interface Eval { - meanSquaredError: number | ''; - rSquared: number | ''; + meanSquaredError: number | string; + rSquared: number | string; error: null | string; } @@ -119,6 +132,13 @@ export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; }; +export const isRegressionResultsSearchBoolQuery = ( + arg: any +): arg is RegressionResultsSearchBoolQuery => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === 'bool'; +}; + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; // Description attribute is not supported yet @@ -212,6 +232,42 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) { return { meanSquaredError, rSquared }; } +interface RegressionResultsSearchBoolQuery { + bool: Dictionary; +} +interface RegressionResultsSearchTermQuery { + term: Dictionary; +} + +export type RegressionResultsSearchQuery = + | RegressionResultsSearchBoolQuery + | RegressionResultsSearchTermQuery + | SavedSearchQuery; + +export function getEvalQueryBody({ + resultsField, + isTraining, + searchQuery, + ignoreDefaultQuery, +}: { + resultsField: string; + isTraining: boolean; + searchQuery?: RegressionResultsSearchQuery; + ignoreDefaultQuery?: boolean; +}) { + let query: RegressionResultsSearchQuery = { + term: { [`${resultsField}.is_training`]: { value: isTraining } }, + }; + + if (searchQuery !== undefined && ignoreDefaultQuery === true) { + query = searchQuery; + } else if (isRegressionResultsSearchBoolQuery(searchQuery)) { + const searchQueryClone = cloneDeep(searchQuery); + searchQueryClone.bool.must.push(query); + query = searchQueryClone; + } + return query; +} export const loadEvalData = async ({ isTraining, @@ -219,12 +275,16 @@ export const loadEvalData = async ({ dependentVariable, resultsField, predictionFieldName, + searchQuery, + ignoreDefaultQuery, }: { isTraining: boolean; index: string; dependentVariable: string; resultsField: string; predictionFieldName?: string; + searchQuery?: RegressionResultsSearchQuery; + ignoreDefaultQuery?: boolean; }) => { const results: LoadEvaluateResult = { success: false, eval: null, error: null }; const defaultPredictionField = `${dependentVariable}_prediction`; @@ -232,7 +292,7 @@ export const loadEvalData = async ({ predictionFieldName ? predictionFieldName : defaultPredictionField }`; - const query = { term: { [`${resultsField}.is_training`]: { value: isTraining } } }; + const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery }); const config = { index, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/fields.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/fields.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts new file mode 100644 index 0000000000000..02a1c30259cce --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getAnalysisType, + getDependentVar, + getPredictionFieldName, + isOutlierAnalysis, + refreshAnalyticsList$, + useRefreshAnalyticsList, + DataFrameAnalyticsId, + DataFrameAnalyticsConfig, + IndexName, + IndexPattern, + REFRESH_ANALYTICS_LIST_STATE, + ANALYSIS_CONFIG_TYPE, + RegressionEvaluateResponse, + getValuesFromResponse, + loadEvalData, + Eval, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, +} from './analytics'; + +export { + getDefaultSelectableFields, + getDefaultRegressionFields, + getFlattenedFields, + sortColumns, + sortRegressionResultsColumns, + sortRegressionResultsFields, + toggleSelectedField, + EsId, + EsDoc, + EsDocSource, + EsFieldName, + MAX_COLUMNS, +} from './fields'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx similarity index 77% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index 11bb62dec1624..c4bba08353d84 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; @@ -18,13 +18,16 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiPanel, EuiPopover, EuiPopoverTitle, EuiProgress, + EuiSpacer, EuiText, EuiTitle, EuiToolTip, + Query, } from '@elastic/eui'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; @@ -32,7 +35,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import { ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, OnTableChangeArg, SortingPropType, SORT_DIRECTION, @@ -51,12 +54,18 @@ import { EsDoc, MAX_COLUMNS, INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, } from '../../../../common'; import { getOutlierScoreFieldName } from './common'; -import { useExploreData } from './use_explore_data'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { useExploreData, TableItem } from './use_explore_data'; +import { + DATA_FRAME_TASK_STATE, + Query as QueryType, +} from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; const customColorScaleFactory = (n: number) => (t: number) => { if (t < 1 / n) { @@ -77,7 +86,7 @@ interface GetDataFrameAnalyticsResponse { const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; -const ExplorationTitle: React.SFC<{ jobId: string }> = ({ jobId }) => ( +const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { @@ -99,6 +108,10 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(25); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + useEffect(() => { (async function() { const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( @@ -119,23 +132,9 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ? euiThemeDark : euiThemeLight; - const [clearTable, setClearTable] = useState(false); - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - // EuiInMemoryTable has an issue with dynamic sortable columns - // and will trigger a full page Kibana error in such a case. - // The following is a workaround until this is solved upstream: - // - If the sortable/columns config changes, - // the table will be unmounted/not rendered. - // This is what setClearTable(true) in toggleColumn() does. - // - After that on next render it gets re-enabled. To make sure React - // doesn't consolidate the state updates, setTimeout is used. - if (clearTable) { - setTimeout(() => setClearTable(false), 0); - } - function toggleColumnsPopover() { setColumnsPopoverVisible(!isColumnsPopoverVisible); } @@ -146,7 +145,6 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { function toggleColumn(column: EsFieldName) { if (tableItems.length > 0 && jobConfig !== undefined) { - setClearTable(true); // spread to a new array otherwise the component wouldn't re-render setSelectedFields([...toggleSelectedField(selectedFields, column)]); } @@ -169,7 +167,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { docFieldsCount = docFields.length; } - const columns: ColumnType[] = []; + const columns: Array> = []; if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { // table cell color coding takes into account: @@ -190,7 +188,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { columns.push( ...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => { - const column: ColumnType = { + const column: ColumnType = { field: k, name: k, sortable: true, @@ -309,6 +307,17 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ); } + useEffect(() => { + if (jobConfig !== undefined) { + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + const outlierScoreFieldSelected = selectedFields.includes(outlierScoreFieldName); + + const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; + const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + useEffect(() => { // by default set the sorting to descending on the `outlier_score` field. // if that's not available sort ascending on the first column. @@ -319,7 +328,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction }); + loadExploreData({ field, direction, searchQuery }); return; } }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); @@ -344,8 +353,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { setPageSize(size); if (sort.field !== sortField || sort.direction !== sortDirection) { - setClearTable(true); - loadExploreData(sort); + loadExploreData({ ...sort, searchQuery }); } }; } @@ -358,11 +366,37 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { hidePerPageOptions: false, }; + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + } catch (e) { + setSearchError(e.toString()); + } + } + }; + + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate('xpack.ml.dataframe.analytics.exploration.searchBoxPlaceholder', { + defaultMessage: 'E.g. avg>0.5', + }), + }, + }; + if (jobConfig === undefined) { return null; } - - if (status === INDEX_STATUS.ERROR) { + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( @@ -379,34 +413,20 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ); } - if (status === INDEX_STATUS.LOADED && tableItems.length === 0) { - return ( - - - - - - - {getTaskStateBadge(jobStatus)} - - - -

- {i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - })} -

-
-
- ); + let tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { + tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', + }); } + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( @@ -483,20 +503,38 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { {status !== INDEX_STATUS.LOADING && ( )} - {clearTable === false && columns.length > 0 && sortField !== '' && ( - + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && ( + + {tableItems.length === SEARCH_SIZE && ( + + + + )} + + + )} ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts new file mode 100644 index 0000000000000..e76cbaa463f1d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { SearchResponse } from 'elasticsearch'; + +import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; + +import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; + +import { + getDefaultSelectableFields, + getFlattenedFields, + DataFrameAnalyticsConfig, + EsFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, +} from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +export type TableItem = Record; + +interface LoadExploreDataArg { + field: string; + direction: SortDirection; + searchQuery: SavedSearchQuery; +} + +export interface UseExploreDataReturnType { + errorMessage: string; + loadExploreData: (arg: LoadExploreDataArg) => void; + sortField: EsFieldName; + sortDirection: SortDirection; + status: INDEX_STATUS; + tableItems: TableItem[]; +} + +export const useExploreData = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + selectedFields: EsFieldName[], + setSelectedFields: React.Dispatch> +): UseExploreDataReturnType => { + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [tableItems, setTableItems] = useState([]); + const [sortField, setSortField] = useState(''); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { + if (jobConfig !== undefined) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resultsField = jobConfig.dest.results_field; + + const body: SearchQuery = { + query: searchQuery, + }; + + if (field !== undefined) { + body.sort = [ + { + [field]: { + order: direction, + }, + }, + ]; + } + + const resp: SearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: SEARCH_SIZE, + body, + }); + + setSortField(field); + setSortDirection(direction); + + const docs = resp.hits.hits; + + if (docs.length === 0) { + setTableItems([]); + setStatus(INDEX_STATUS.LOADED); + return; + } + + if (selectedFields.length === 0) { + const newSelectedFields = getDefaultSelectableFields(docs, resultsField); + setSelectedFields(newSelectedFields); + } + + // Create a version of the doc's source with flattened field names. + // This avoids confusion later on if a field name has dots in its name + // or is a nested fields when displaying it via EuiInMemoryTable. + const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); + const transformedTableItems = docs.map(doc => { + const item: TableItem = {}; + flattenedFields.forEach(ff => { + item[ff] = getNestedProperty(doc._source, ff); + if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. + item[ff] = doc._source[`"${ff}"`]; + } + if (item[ff] === undefined) { + const parts = ff.split('.'); + if (parts[0] === resultsField && parts.length >= 2) { + parts.shift(); + if (doc._source[resultsField] !== undefined) { + item[ff] = doc._source[resultsField][parts.join('.')]; + } + } + } + }); + return item; + }); + + setTableItems(transformedTableItems); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e)); + } + setTableItems([]); + setStatus(INDEX_STATUS.ERROR); + } + } + }; + + useEffect(() => { + if (jobConfig !== undefined) { + loadExploreData({ + field: getOutlierScoreFieldName(jobConfig), + direction: SORT_DIRECTION.DESC, + searchQuery: defaultSearchQuery, + }); + } + }, [jobConfig && jobConfig.id]); + + return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss new file mode 100644 index 0000000000000..bb948785d3efa --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss @@ -0,0 +1 @@ +@import 'regression_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss new file mode 100644 index 0000000000000..edcc9870ff93b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss @@ -0,0 +1,3 @@ +.mlDataFrameAnalyticsRegression__evaluateStat { + padding-top: $euiSizeL; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx similarity index 75% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx index d0633d586063a..9765192f0e446 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx @@ -62,6 +62,29 @@ export const ErrorCallout: FC = ({ error }) => {

); + } else if (error.includes('userProvidedQueryBuilder')) { + // query bar syntax is incorrect + errorCallout = ( + +

+ {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', + { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + } + )} +

+
+ ); } return {errorCallout}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx new file mode 100644 index 0000000000000..d877ed40e587d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -0,0 +1,346 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, Fragment, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { ErrorCallout } from './error_callout'; +import { + getValuesFromResponse, + getDependentVar, + getPredictionFieldName, + loadEvalData, + Eval, + DataFrameAnalyticsConfig, +} from '../../../../common'; +import { ml } from '../../../../../services/ml_api_service'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { EvaluateStat } from './evaluate_stat'; +import { + getEvalQueryBody, + isRegressionResultsSearchBoolQuery, + RegressionResultsSearchQuery, + SearchQuery, +} from '../../../../common/analytics'; + +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + searchQuery: RegressionResultsSearchQuery; +} + +interface TrackTotalHitsSearchResponse { + hits: { + total: { + value: number; + relation: string; + }; + hits: any[]; + }; +} + +const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; + +export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const [trainingEval, setTrainingEval] = useState(defaultEval); + const [generalizationEval, setGeneralizationEval] = useState(defaultEval); + const [isLoadingTraining, setIsLoadingTraining] = useState(false); + const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); + const [trainingDocsCount, setTrainingDocsCount] = useState(null); + const [generalizationDocsCount, setGeneralizationDocsCount] = useState(null); + + const index = jobConfig.dest.index; + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + // default is 'ml' + const resultsField = jobConfig.dest.results_field; + + const loadDocsCount = async ({ + ignoreDefaultQuery = true, + isTraining, + }: { + ignoreDefaultQuery?: boolean; + isTraining: boolean; + }): Promise<{ + docsCount: number | null; + success: boolean; + }> => { + const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); + + try { + const body: SearchQuery = { + track_total_hits: true, + query, + }; + + const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: 0, + body, + }); + + const docsCount = resp.hits.total && resp.hits.total.value; + return { docsCount, success: true }; + } catch (e) { + return { + docsCount: null, + success: false, + }; + } + }; + + const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => { + setIsLoadingGeneralization(true); + + const genErrorEval = await loadEvalData({ + isTraining: false, + index, + dependentVariable, + resultsField, + predictionFieldName, + searchQuery, + ignoreDefaultQuery, + }); + + if (genErrorEval.success === true && genErrorEval.eval) { + const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + setGeneralizationEval({ + meanSquaredError, + rSquared, + error: null, + }); + setIsLoadingGeneralization(false); + } else { + setIsLoadingGeneralization(false); + setGeneralizationEval({ + meanSquaredError: '', + rSquared: '', + error: genErrorEval.error, + }); + } + }; + + const loadTrainingData = async (ignoreDefaultQuery: boolean = true) => { + setIsLoadingTraining(true); + + const trainingErrorEval = await loadEvalData({ + isTraining: true, + index, + dependentVariable, + resultsField, + predictionFieldName, + searchQuery, + ignoreDefaultQuery, + }); + + if (trainingErrorEval.success === true && trainingErrorEval.eval) { + const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + setTrainingEval({ + meanSquaredError, + rSquared, + error: null, + }); + setIsLoadingTraining(false); + } else { + setIsLoadingTraining(false); + setTrainingEval({ + meanSquaredError: '', + rSquared: '', + error: trainingErrorEval.error, + }); + } + }; + + const loadData = async ({ + isTrainingClause, + }: { + isTrainingClause?: { query: string; operator: string }; + }) => { + // searchBar query is filtering for testing data + if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { + loadGeneralizationData(); + + const docsCountResp = await loadDocsCount({ isTraining: false }); + if (docsCountResp.success === true) { + setGeneralizationDocsCount(docsCountResp.docsCount); + } else { + setGeneralizationDocsCount(null); + } + + setTrainingDocsCount(0); + setTrainingEval({ + meanSquaredError: '--', + rSquared: '--', + error: null, + }); + } else if (isTrainingClause !== undefined && isTrainingClause.query === 'true') { + // searchBar query is filtering for training data + loadTrainingData(); + + const docsCountResp = await loadDocsCount({ isTraining: true }); + if (docsCountResp.success === true) { + setTrainingDocsCount(docsCountResp.docsCount); + } else { + setTrainingDocsCount(null); + } + + setGeneralizationDocsCount(0); + setGeneralizationEval({ + meanSquaredError: '--', + rSquared: '--', + error: null, + }); + } else { + // No is_training clause/filter from search bar so load both + loadGeneralizationData(false); + const genDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: false, + }); + if (genDocsCountResp.success === true) { + setGeneralizationDocsCount(genDocsCountResp.docsCount); + } else { + setGeneralizationDocsCount(null); + } + + loadTrainingData(false); + const trainDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: true, + }); + if (trainDocsCountResp.success === true) { + setTrainingDocsCount(trainDocsCountResp.docsCount); + } else { + setTrainingDocsCount(null); + } + } + }; + + useEffect(() => { + const hasIsTrainingClause = + isRegressionResultsSearchBoolQuery(searchQuery) && + searchQuery.bool.must.filter( + (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined + ); + const isTrainingClause = + hasIsTrainingClause && + hasIsTrainingClause[0] && + hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + + loadData({ isTrainingClause }); + }, [JSON.stringify(searchQuery)]); + + return ( + + + + + + {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { + defaultMessage: 'Regression job ID {jobId}', + values: { jobId: jobConfig.id }, + })} + + + + + {getTaskStateBadge(jobStatus)} + + + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle', + { + defaultMessage: 'Generalization error', + } + )} + + + {generalizationDocsCount !== null && ( + + + + )} + + + {generalizationEval.error !== null && } + {generalizationEval.error === null && ( + + + + + + + + + )} + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle', + { + defaultMessage: 'Training error', + } + )} + + + {trainingDocsCount !== null && ( + + + + )} + + + {trainingEval.error !== null && } + {trainingEval.error === null && ( + + + + + + + + + )} + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx new file mode 100644 index 0000000000000..692a2afc729d5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface Props { + isLoading: boolean; + title: number | string; + isMSE: boolean; +} + +const meanSquaredErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', + { + defaultMessage: 'Mean squared error', + } +); +const rSquaredText = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', + { + defaultMessage: 'R squared', + } +); +const meanSquaredErrorTooltipContent = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', + { + defaultMessage: + 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', + } +); +const rSquaredTooltipContent = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', + { + defaultMessage: + 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', + } +); + +export const EvaluateStat: FC = ({ isLoading, isMSE, title }) => ( + + + + + + + + +); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx similarity index 88% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 7beea07f9502d..2f7ff4feed2a8 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -12,6 +12,7 @@ import { DataFrameAnalyticsConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; interface GetDataFrameAnalyticsResponse { count: number; @@ -24,7 +25,7 @@ const LoadingPanel: FC = () => (
); -export const ExplorationTitle: React.SFC<{ jobId: string }> = ({ jobId }) => ( +export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { @@ -44,6 +45,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -98,12 +100,16 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { {isLoadingJobConfig === true && jobConfig === undefined && } {isLoadingJobConfig === false && jobConfig !== undefined && ( - + )} {isLoadingJobConfig === true && jobConfig === undefined && } {isLoadingJobConfig === false && jobConfig !== undefined && ( - + )} ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx new file mode 100644 index 0000000000000..37c2e40c89c3c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -0,0 +1,482 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { Fragment, FC, useEffect, useState } from 'react'; +import moment from 'moment-timezone'; + +import { i18n } from '@kbn/i18n'; +import { + EuiBadge, + EuiButtonIcon, + EuiCallOut, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiPopover, + EuiPopoverTitle, + EuiProgress, + EuiSpacer, + EuiText, + EuiToolTip, + Query, +} from '@elastic/eui'; + +import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; + +import { + ColumnType, + mlInMemoryTableBasicFactory, + OnTableChangeArg, + SortingPropType, + SORT_DIRECTION, +} from '../../../../../components/ml_in_memory_table'; + +import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + sortRegressionResultsColumns, + sortRegressionResultsFields, + toggleSelectedField, + DataFrameAnalyticsConfig, + EsFieldName, + EsDoc, + MAX_COLUMNS, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { useExploreData, TableItem } from './use_explore_data'; +import { ExplorationTitle } from './regression_exploration'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch>; +} + +export const ResultsTable: FC = React.memo( + ({ jobConfig, jobStatus, setEvaluateSearchQuery }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + + function toggleColumnsPopover() { + setColumnsPopoverVisible(!isColumnsPopoverVisible); + } + + function closeColumnsPopover() { + setColumnsPopoverVisible(false); + } + + function toggleColumn(column: EsFieldName) { + if (tableItems.length > 0 && jobConfig !== undefined) { + // spread to a new array otherwise the component wouldn't re-render + setSelectedFields([...toggleSelectedField(selectedFields, column)]); + } + } + + const { + errorMessage, + loadExploreData, + sortField, + sortDirection, + status, + tableItems, + } = useExploreData(jobConfig, selectedFields, setSelectedFields); + + let docFields: EsFieldName[] = []; + let docFieldsCount = 0; + if (tableItems.length > 0) { + docFields = Object.keys(tableItems[0]); + docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); + docFieldsCount = docFields.length; + } + + const columns: Array> = []; + + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { + columns.push( + ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { + const column: ColumnType = { + field: k, + name: k, + sortable: true, + truncateText: true, + }; + + const render = (d: any, fullItem: EsDoc) => { + if (Array.isArray(d) && d.every(item => typeof item === 'string')) { + // If the cells data is an array of strings, return as a comma separated list. + // The list will get limited to 5 items with `…` at the end if there's more in the original array. + return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; + } else if (Array.isArray(d)) { + // If the cells data is an array of e.g. objects, display a 'array' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', + { + defaultMessage: 'array', + } + )} + + + ); + } else if (typeof d === 'object' && d !== null) { + // If the cells data is an object, display a 'object' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent', + { + defaultMessage: 'object', + } + )} + + + ); + } + + return d; + }; + + let columnType; + + if (tableItems.length > 0) { + columnType = typeof tableItems[0][k]; + } + + if (typeof columnType !== 'undefined') { + switch (columnType) { + case 'boolean': + column.dataType = 'boolean'; + break; + case 'Date': + column.align = 'right'; + column.render = (d: any) => + formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case 'number': + column.dataType = 'number'; + column.render = render; + break; + default: + column.render = render; + break; + } + } else { + column.render = render; + } + + return column; + }) + ); + } + + useEffect(() => { + if (jobConfig !== undefined) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + + useEffect(() => { + // by default set the sorting to descending on the prediction field (`_prediction`). + // if that's not available sort ascending on the first column. + // also check if the current sorting field is still available. + if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); + + let sorting: SortingPropType = false; + let onTableChange; + + if (columns.length > 0 && sortField !== '' && sortField !== undefined) { + sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: sortField, direction: sortDirection }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + if (sort.field !== sortField || sort.direction !== sortDirection) { + loadExploreData({ ...sort, searchQuery }); + } + }; + } + + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: tableItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + hidePerPageOptions: false, + }; + + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + // set query for use in evaluate panel + setEvaluateSearchQuery(esQueryDsl); + } catch (e) { + setSearchError(e.toString()); + } + } + }; + + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', + { + defaultMessage: 'E.g. avg>0.5', + } + ), + }, + filters: [ + { + type: 'field_value_toggle_group', + field: `${jobConfig.dest.results_field}.is_training`, + items: [ + { + value: false, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', + { + defaultMessage: 'Testing', + } + ), + }, + { + value: true, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', + { + defaultMessage: 'Training', + } + ), + }, + ], + }, + ], + }; + + if (jobConfig === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + + + + + + + {getTaskStateBadge(jobStatus)} + + + +

{errorMessage}

+
+
+ ); + } + + const tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + + return ( + + + + + + + + + {getTaskStateBadge(jobStatus)} + + + + + + + {docFieldsCount > MAX_COLUMNS && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + } + )} + + )} + + + + + } + isOpen={isColumnsPopoverVisible} + closePopover={closeColumnsPopover} + ownFocus + > + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', + { + defaultMessage: 'Select fields', + } + )} + +
+ {docFields.map(d => ( + toggleColumn(d)} + disabled={selectedFields.includes(d) && selectedFields.length === 1} + /> + ))} +
+
+
+
+
+
+
+ {status === INDEX_STATUS.LOADING && } + {status !== INDEX_STATUS.LOADING && ( + + )} + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + + {tableItems.length === SEARCH_SIZE && ( + + + + )} + + + + )} +
+ ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts new file mode 100644 index 0000000000000..3a83ad238d0e1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { useEffect, useState } from 'react'; + +import { SearchResponse } from 'elasticsearch'; + +import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; + +import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + getDefaultRegressionFields, + getFlattenedFields, + DataFrameAnalyticsConfig, + EsFieldName, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, +} from '../../../../common'; + +export type TableItem = Record; + +interface LoadExploreDataArg { + field: string; + direction: SortDirection; + searchQuery: SavedSearchQuery; +} +export interface UseExploreDataReturnType { + errorMessage: string; + loadExploreData: (arg: LoadExploreDataArg) => void; + sortField: EsFieldName; + sortDirection: SortDirection; + status: INDEX_STATUS; + tableItems: TableItem[]; +} + +export const useExploreData = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + selectedFields: EsFieldName[], + setSelectedFields: React.Dispatch> +): UseExploreDataReturnType => { + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [tableItems, setTableItems] = useState([]); + const [sortField, setSortField] = useState(''); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { + if (jobConfig !== undefined) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resultsField = jobConfig.dest.results_field; + const body: SearchQuery = { + query: searchQuery, + }; + + if (field !== undefined) { + body.sort = [ + { + [field]: { + order: direction, + }, + }, + ]; + } + + const resp: SearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: SEARCH_SIZE, + body, + }); + + setSortField(field); + setSortDirection(direction); + + const docs = resp.hits.hits; + + if (docs.length === 0) { + setTableItems([]); + setStatus(INDEX_STATUS.LOADED); + return; + } + + if (selectedFields.length === 0) { + const newSelectedFields = getDefaultRegressionFields(docs, jobConfig); + setSelectedFields(newSelectedFields); + } + + // Create a version of the doc's source with flattened field names. + // This avoids confusion later on if a field name has dots in its name + // or is a nested fields when displaying it via EuiInMemoryTable. + const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); + const transformedTableItems = docs.map(doc => { + const item: TableItem = {}; + flattenedFields.forEach(ff => { + item[ff] = getNestedProperty(doc._source, ff); + if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. + item[ff] = doc._source[`"${ff}"`]; + } + if (item[ff] === undefined) { + const parts = ff.split('.'); + if (parts[0] === resultsField && parts.length >= 2) { + parts.shift(); + if (doc._source[resultsField] !== undefined) { + item[ff] = doc._source[resultsField][parts.join('.')]; + } + } + } + }); + return item; + }); + + setTableItems(transformedTableItems); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e)); + } + setTableItems([]); + setStatus(INDEX_STATUS.ERROR); + } + } + }; + + useEffect(() => { + if (jobConfig !== undefined) { + loadExploreData({ + field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis), + direction: SORT_DIRECTION.DESC, + searchQuery: defaultSearchQuery, + }); + } + }, [jobConfig && jobConfig.id]); + + return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx new file mode 100644 index 0000000000000..c41285f40d64b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { IndexPatterns } from 'ui/index_patterns'; +import { I18nContext } from 'ui/i18n'; + +import { InjectorService } from '../../../../../common/types/angular'; +import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; + +import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; + +import { Page } from './page'; + +module.directive('mlDataFrameAnalyticsExploration', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const globalState = $injector.get('globalState'); + globalState.fetch(); + + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + ReactDOM.render( + + + + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/route.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx similarity index 99% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 6de278cda16e6..f98ce486f7337 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -34,7 +34,7 @@ import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; import { ProgressBar, - MlInMemoryTable, + mlInMemoryTableFactory, OnTableChangeArg, SortDirection, SORT_DIRECTION, @@ -326,6 +326,8 @@ export const DataFrameAnalyticsList: FC = ({ setSortDirection(direction); }; + const MlInMemoryTable = mlInMemoryTableFactory(); + return ( diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index e639f32116d4a..fc860251bf83d 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; import { JobMessages } from '../../../../../components/job_messages'; -import { JobMessage } from '../../../../../../common/types/audit_message'; +import { JobMessage } from '../../../../../../../common/types/audit_message'; interface Props { analyticsId: string; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts index cfd900b303aa3..4ccfa8a562c6c 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts @@ -11,7 +11,7 @@ import { timefilter } from 'ui/timefilter'; import { DEFAULT_REFRESH_INTERVAL_MS, MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../../common/constants/jobs_list'; +} from '../../../../../../../common/constants/jobs_list'; import { useRefreshAnalyticsList } from '../../../../common'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index 027acf6fa2e79..15f30b6cca6c4 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsButton } from './create_analytics_button'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx index 2bcc7305c5df7..880a1354e7a64 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsFlyout } from './create_analytics_flyout'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx index 846397aa93929..592b53dcecba0 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsForm } from './create_analytics_form'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx similarity index 99% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 598f88387f410..47af274424c44 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { metadata } from 'ui/metadata'; import { IndexPattern, INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns'; import { ml } from '../../../../../services/ml_api_service'; -import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; +import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useKibanaContext } from '../../../../../contexts/kibana'; @@ -32,7 +32,7 @@ import { DEFAULT_MODEL_MEMORY_LIMIT, getJobConfigFromFormState, } from '../../hooks/use_create_analytics_form/state'; -import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { Messages } from './messages'; import { JobType } from './job_type'; import { mmlUnitInvalidErrorMessage } from '../../hooks/use_create_analytics_form/reducer'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx new file mode 100644 index 0000000000000..8299ff53393bb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { IndexPatterns } from 'ui/index_patterns'; +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../../common/types/angular'; +import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; + +import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; + +import { Page } from './page'; + +module.directive('mlDataFrameAnalyticsManagement', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + ReactDOM.render( + + + + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts new file mode 100644 index 0000000000000..56d09169a3c39 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -0,0 +1,336 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { idx } from '@kbn/elastic-idx'; +import { i18n } from '@kbn/i18n'; + +import { validateIndexPattern } from 'ui/index_patterns'; + +import { isValidIndexName } from '../../../../../../../common/util/es_utils'; + +import { Action, ACTION } from './actions'; +import { getInitialState, getJobConfigFromFormState, State, JOB_TYPES } from './state'; +import { + isJobIdValid, + validateModelMemoryLimitUnits, +} from '../../../../../../../common/util/job_utils'; +import { maxLengthValidator } from '../../../../../../../common/util/validators'; +import { + JOB_ID_MAX_LENGTH, + ALLOWED_DATA_UNITS, +} from '../../../../../../../common/constants/validation'; +import { getDependentVar, isRegressionAnalysis } from '../../../../common/analytics'; + +const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( + ', ' +)} or ${[...ALLOWED_DATA_UNITS].pop()}`; + +export const mmlUnitInvalidErrorMessage = i18n.translate( + 'xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', + { + defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}', + values: { str: mmlAllowedUnitsStr }, + } +); + +const getSourceIndexString = (state: State) => { + const { jobConfig } = state; + + const sourceIndex = idx(jobConfig, _ => _.source.index); + + if (typeof sourceIndex === 'string') { + return sourceIndex; + } + + if (Array.isArray(sourceIndex)) { + return sourceIndex.join(','); + } + + return ''; +}; + +export const validateAdvancedEditor = (state: State): State => { + const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form; + const { jobConfig } = state; + + state.advancedEditorMessages = []; + + const sourceIndexName = getSourceIndexString(state); + const sourceIndexNameEmpty = sourceIndexName === ''; + // general check against Kibana index pattern names, but since this is about the advanced editor + // with support for arrays in the job config, we also need to check that each individual name + // doesn't include a comma if index names are supplied as an array. + // `validateIndexPattern()` returns a map of messages, we're only interested here if it's valid or not. + // If there are no messages, it means the index pattern is valid. + let sourceIndexNameValid = Object.keys(validateIndexPattern(sourceIndexName)).length === 0; + const sourceIndex = idx(jobConfig, _ => _.source.index); + if (sourceIndexNameValid) { + if (typeof sourceIndex === 'string') { + sourceIndexNameValid = !sourceIndex.includes(','); + } + if (Array.isArray(sourceIndex)) { + sourceIndexNameValid = !sourceIndex.some(d => d.includes(',')); + } + } + + const destinationIndexName = idx(jobConfig, _ => _.dest.index) || ''; + const destinationIndexNameEmpty = destinationIndexName === ''; + const destinationIndexNameValid = isValidIndexName(destinationIndexName); + const destinationIndexPatternTitleExists = + state.indexPatternsMap[destinationIndexName] !== undefined; + const mml = jobConfig.model_memory_limit; + const modelMemoryLimitEmpty = mml === ''; + if (!modelMemoryLimitEmpty && mml !== undefined) { + const { valid } = validateModelMemoryLimitUnits(mml); + state.form.modelMemoryLimitUnitValid = valid; + } + + let dependentVariableEmpty = false; + if (isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; + dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariableName === ''; + } + + if (sourceIndexNameEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty', + { + defaultMessage: 'The source index name must not be empty.', + } + ), + message: '', + }); + } else if (!sourceIndexNameValid) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameValid', + { + defaultMessage: 'Invalid source index name.', + } + ), + message: '', + }); + } + + if (destinationIndexNameEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty', + { + defaultMessage: 'The destination index name must not be empty.', + } + ), + message: '', + }); + } else if (!destinationIndexNameValid) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid', + { + defaultMessage: 'Invalid destination index name.', + } + ), + message: '', + }); + } + + if (dependentVariableEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.dependentVariableEmpty', + { + defaultMessage: 'The dependent variable field must not be empty.', + } + ), + message: '', + }); + } + + if (modelMemoryLimitEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty', + { + defaultMessage: 'The model memory limit field must not be empty.', + } + ), + message: '', + }); + } + + if (!state.form.modelMemoryLimitUnitValid) { + state.advancedEditorMessages.push({ + error: mmlUnitInvalidErrorMessage, + message: '', + }); + } + + state.isValid = + state.form.modelMemoryLimitUnitValid && + !jobIdEmpty && + jobIdValid && + !jobIdExists && + !sourceIndexNameEmpty && + sourceIndexNameValid && + !destinationIndexNameEmpty && + destinationIndexNameValid && + !dependentVariableEmpty && + !modelMemoryLimitEmpty && + (!destinationIndexPatternTitleExists || !createIndexPattern); + + return state; +}; + +const validateForm = (state: State): State => { + const { + jobIdEmpty, + jobIdValid, + jobIdExists, + jobType, + sourceIndexNameEmpty, + sourceIndexNameValid, + destinationIndexNameEmpty, + destinationIndexNameValid, + destinationIndexPatternTitleExists, + createIndexPattern, + dependentVariable, + modelMemoryLimit, + } = state.form; + + const dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariable === ''; + const modelMemoryLimitEmpty = modelMemoryLimit === ''; + + if (!modelMemoryLimitEmpty && modelMemoryLimit !== undefined) { + const { valid } = validateModelMemoryLimitUnits(modelMemoryLimit); + state.form.modelMemoryLimitUnitValid = valid; + } + + state.isValid = + state.form.modelMemoryLimitUnitValid && + !jobIdEmpty && + jobIdValid && + !jobIdExists && + !sourceIndexNameEmpty && + sourceIndexNameValid && + !destinationIndexNameEmpty && + destinationIndexNameValid && + !dependentVariableEmpty && + !modelMemoryLimitEmpty && + (!destinationIndexPatternTitleExists || !createIndexPattern); + + return state; +}; + +export function reducer(state: State, action: Action): State { + switch (action.type) { + case ACTION.ADD_REQUEST_MESSAGE: + const requestMessages = state.requestMessages; + requestMessages.push(action.requestMessage); + return { ...state, requestMessages }; + + case ACTION.RESET_REQUEST_MESSAGES: + return { ...state, requestMessages: [] }; + + case ACTION.CLOSE_MODAL: + return { ...state, isModalVisible: false }; + + case ACTION.OPEN_MODAL: + return { ...state, isModalVisible: true }; + + case ACTION.RESET_ADVANCED_EDITOR_MESSAGES: + return { ...state, advancedEditorMessages: [] }; + + case ACTION.RESET_FORM: + return getInitialState(); + + case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: + return { ...state, advancedEditorRawString: action.advancedEditorRawString }; + + case ACTION.SET_FORM_STATE: + const newFormState = { ...state.form, ...action.payload }; + + // update state attributes which are derived from other state attributes. + if (action.payload.destinationIndex !== undefined) { + newFormState.destinationIndexNameExists = state.indexNames.some( + name => newFormState.destinationIndex === name + ); + newFormState.destinationIndexNameEmpty = newFormState.destinationIndex === ''; + newFormState.destinationIndexNameValid = isValidIndexName(newFormState.destinationIndex); + newFormState.destinationIndexPatternTitleExists = + state.indexPatternsMap[newFormState.destinationIndex] !== undefined; + } + + if (action.payload.jobId !== undefined) { + newFormState.jobIdExists = state.jobIds.some(id => newFormState.jobId === id); + newFormState.jobIdEmpty = newFormState.jobId === ''; + newFormState.jobIdValid = isJobIdValid(newFormState.jobId); + newFormState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)( + newFormState.jobId + ); + } + + if (action.payload.sourceIndex !== undefined) { + newFormState.sourceIndexNameEmpty = newFormState.sourceIndex === ''; + const validationMessages = validateIndexPattern(newFormState.sourceIndex); + newFormState.sourceIndexNameValid = Object.keys(validationMessages).length === 0; + } + + return state.isAdvancedEditorEnabled + ? validateAdvancedEditor({ ...state, form: newFormState }) + : validateForm({ ...state, form: newFormState }); + + case ACTION.SET_INDEX_NAMES: { + const newState = { ...state, indexNames: action.indexNames }; + newState.form.destinationIndexNameExists = newState.indexNames.some( + name => newState.form.destinationIndex === name + ); + return newState; + } + + case ACTION.SET_INDEX_PATTERN_TITLES: { + const newState = { + ...state, + ...action.payload, + }; + newState.form.destinationIndexPatternTitleExists = + newState.indexPatternsMap[newState.form.destinationIndex] !== undefined; + return newState; + } + + case ACTION.SET_IS_JOB_CREATED: + return { ...state, isJobCreated: action.isJobCreated }; + + case ACTION.SET_IS_JOB_STARTED: + return { ...state, isJobStarted: action.isJobStarted }; + + case ACTION.SET_IS_MODAL_BUTTON_DISABLED: + return { ...state, isModalButtonDisabled: action.isModalButtonDisabled }; + + case ACTION.SET_IS_MODAL_VISIBLE: + return { ...state, isModalVisible: action.isModalVisible }; + + case ACTION.SET_JOB_CONFIG: + return validateAdvancedEditor({ ...state, jobConfig: action.payload }); + + case ACTION.SET_JOB_IDS: { + const newState = { ...state, jobIds: action.jobIds }; + newState.form.jobIdExists = newState.jobIds.some(id => newState.form.jobId === id); + return newState; + } + + case ACTION.SWITCH_TO_ADVANCED_EDITOR: + const jobConfig = getJobConfigFromFormState(state.form); + return validateAdvancedEditor({ + ...state, + advancedEditorRawString: JSON.stringify(jobConfig, null, 2), + isAdvancedEditorEnabled: true, + jobConfig, + }); + } + + return state; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index b90317015c8c9..f911b5a45e158 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DeepPartial } from '../../../../../../common/types/common'; +import { DeepPartial } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/route.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts new file mode 100644 index 0000000000000..a4d1fd37bc338 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; + +export function getDataVisualizerBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; +} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_file_datavisualizer.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_file_datavisualizer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_file_datavisualizer.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_file_datavisualizer.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts new file mode 100644 index 0000000000000..e8dd89f5db264 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../../breadcrumbs'; + +export function getFileDataVisualizerBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_about_panel.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_about_panel.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_about_panel.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_about_panel.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/about_panel.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/about_panel.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/welcome_content.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/welcome_content.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/analysis_summary.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/analysis_summary.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/analysis_summary.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/analysis_summary.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/edit_flyout.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/edit_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/edit_flyout.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/edit_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/option_lists.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/option_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/option_lists.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/option_lists.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/options.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/options.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/options.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/options.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.test.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.test.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides_validation.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides_validation.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides_validation.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides_validation.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/experimental_badge.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/experimental_badge.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/experimental_badge.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/experimental_badge.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_fields_stats.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_fields_stats.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_fields_stats.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_fields_stats.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/field_stats_card.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/field_stats_card.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/fields_stats.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/fields_stats.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js index e1a5bfba3fd1e..c64a695772dde 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/fields_stats.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js @@ -11,7 +11,7 @@ import React, { import { FieldStatsCard } from './field_stats_card'; import { getFieldNames } from './get_field_names'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; export class FieldsStats extends Component { diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/get_field_names.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/get_field_names.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_file_contents.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_file_contents.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_file_contents.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_file_contents.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/file_contents.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/file_contents.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/file_contents.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/file_contents.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index 852f068ef419f..1f249dcdb0128 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -23,7 +23,7 @@ import { ResultsView } from '../results_view'; import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ImportView } from '../import_view'; -import { MAX_BYTES } from '../../../../../common/constants/file_datavisualizer'; +import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer'; import { readFile, createUrlOverrides, diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/errors.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/errors.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/import_progress.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/import_progress.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/advanced.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/advanced.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/import_settings.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/import_settings.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/simple.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/simple.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_import_sumary.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_import_sumary.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_import_sumary.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_import_sumary.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/import_summary.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/import_summary.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/import_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/csv_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/csv_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js index ac03bec8c2534..a3850b3def18d 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/csv_importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public'; import { Importer } from './importer'; import Papa from 'papaparse'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer_factory.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer_factory.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer_factory.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer_factory.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/sst_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/sst_importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/sst_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/sst_importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/results_links.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_results_view.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_results_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_results_view.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_results_view.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/results_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/results_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/overrides.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/overrides.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/overrides.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index eefa9e461697e..3776245d90c81 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -12,7 +12,7 @@ import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore -import { FileDataVisualizerView } from './components/file_datavisualizer_view'; +import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { indexPatterns: IndexPatterns; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx index 6a147aa1a991b..291e03a96e85f 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx @@ -17,7 +17,7 @@ import uiRoutes from 'ui/routes'; import { IndexPatterns } from 'ui/index_patterns'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs'; -import { InjectorService } from '../../../common/types/angular'; +import { InjectorService } from '../../../../common/types/angular'; import { checkBasicLicense } from '../../license/check_license'; import { checkFindFileStructurePrivilege } from '../../privilege/check_privilege'; import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts new file mode 100644 index 0000000000000..aba45e04c638f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + // @ts-ignore +} from '../../../breadcrumbs'; + +export function getDataVisualizerBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/field_vis_config.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts similarity index 88% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/field_vis_config.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts index 55b8f13631611..bf39cbb90e8f3 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/field_vis_config.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; // The internal representation of the configuration used to build the visuals // which display the field information. diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts new file mode 100644 index 0000000000000..9a886cbc899c2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; + +export interface FieldRequestConfig { + fieldName?: string; + type: ML_JOB_FIELD_TYPES; + cardinality: number; +} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_field_data_card.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/field_data_card.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/field_data_card.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx index d162d166e8f6b..0493beed92482 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/field_data_card.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx @@ -6,11 +6,11 @@ import React, { FC } from 'react'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldVisConfig } from '../../common'; // @ts-ignore -import { FieldTitleBar } from '../../../../components/field_title_bar'; +import { FieldTitleBar } from '../../../../components/field_title_bar/index'; import { BooleanContent, DateContent, diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/field_types_select.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/field_types_select.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx index 80a6f3d2d6743..6fd08076e1f46 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/field_types_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; interface Props { fieldTypes: ML_JOB_FIELD_TYPES[]; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/fields_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/fields_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx index f0b163c5c9e29..e32d1ca17e12e 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/fields_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldDataCard } from '../field_data_card'; import { FieldTypesSelect } from '../field_types_select'; import { FieldVisConfig } from '../../common'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/search_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index 267be982dead4..a43b680720a2a 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -23,11 +23,11 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from 'ui/index_patterns'; -import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; import { SavedSearchQuery } from '../../../../contexts/kibana'; // @ts-ignore -import { KqlFilterBar } from '../../../../components/kql_filter_bar'; +import { KqlFilterBar } from '../../../../components/kql_filter_bar/index'; interface Props { indexPattern: IndexPattern; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/data_loader.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index f0bb998a27614..fe0d69fdeec6b 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -10,7 +10,7 @@ import { toastNotifications } from 'ui/notify'; import { IndexPattern } from 'ui/index_patterns'; import { SavedSearchQuery } from '../../../contexts/kibana'; -import { IndexPatternTitle } from '../../../../common/types/kibana'; +import { IndexPatternTitle } from '../../../../../common/types/kibana'; import { ml } from '../../../services/ml_api_service'; import { FieldRequestConfig } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx new file mode 100644 index 0000000000000..58cd1c2c6fd0c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { I18nContext } from 'ui/i18n'; +import { IndexPatterns } from 'ui/index_patterns'; +import { InjectorService } from '../../../../common/types/angular'; + +import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; +import { createSearchItems } from '../../jobs/new_job/utils/new_job_utils'; + +import { Page } from './page'; + +module.directive('mlDataVisualizer', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + ReactDOM.render( + + + + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx new file mode 100644 index 0000000000000..642b4c5649a13 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -0,0 +1,676 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, Fragment, useEffect, useState } from 'react'; +import { merge } from 'rxjs'; +import { i18n } from '@kbn/i18n'; + +import { FieldType } from 'ui/index_patterns'; +import { timefilter } from 'ui/timefilter'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { KBN_FIELD_TYPES, esQuery } from '../../../../../../../../src/plugins/data/public'; +import { NavigationMenu } from '../../components/navigation_menu'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; +import { isFullLicense } from '../../license/check_license'; +import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; +import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; +import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; +import { kbnTypeToMLJobType } from '../../util/field_types_utils'; +import { timeBasedIndexCheck } from '../../util/index_utils'; +import { TimeBuckets } from '../../util/time_buckets'; +import { FieldRequestConfig, FieldVisConfig } from './common'; +import { ActionsPanel } from './components/actions_panel'; +import { FieldsPanel } from './components/fields_panel'; +import { SearchPanel } from './components/search_panel'; +import { DataLoader } from './data_loader'; + +interface DataVisualizerPageState { + searchQuery: string | SavedSearchQuery; + searchString: string | SavedSearchQuery; + searchQueryLanguage: SEARCH_QUERY_LANGUAGE; + samplerShardSize: number; + overallStats: any; + metricConfigs: FieldVisConfig[]; + totalMetricFieldCount: number; + populatedMetricFieldCount: number; + showAllMetrics: boolean; + metricFieldQuery?: string; + nonMetricConfigs: FieldVisConfig[]; + totalNonMetricFieldCount: number; + populatedNonMetricFieldCount: number; + showAllNonMetrics: boolean; + nonMetricShowFieldType: ML_JOB_FIELD_TYPES | '*'; + nonMetricFieldQuery?: string; +} + +const defaultSearchQuery = { + match_all: {}, +}; + +function getDefaultPageState(): DataVisualizerPageState { + return { + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + samplerShardSize: 5000, + overallStats: { + totalCount: 0, + aggregatableExistsFields: [], + aggregatableNotExistsFields: [], + nonAggregatableExistsFields: [], + nonAggregatableNotExistsFields: [], + }, + metricConfigs: [], + totalMetricFieldCount: 0, + populatedMetricFieldCount: 0, + showAllMetrics: false, + nonMetricConfigs: [], + totalNonMetricFieldCount: 0, + populatedNonMetricFieldCount: 0, + showAllNonMetrics: false, + nonMetricShowFieldType: '*', + }; +} + +export const Page: FC = () => { + const kibanaContext = useKibanaContext(); + + const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = kibanaContext; + + const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); + + useEffect(() => { + if (currentIndexPattern.timeFieldName !== undefined) { + timefilter.enableTimeRangeSelector(); + } else { + timefilter.disableTimeRangeSelector(); + } + + timefilter.enableAutoRefreshSelector(); + timeBasedIndexCheck(currentIndexPattern, true); + }, []); + + // Obtain the list of non metric field types which appear in the index pattern. + let indexedFieldTypes: ML_JOB_FIELD_TYPES[] = []; + const indexPatternFields: FieldType[] = currentIndexPattern.fields; + indexPatternFields.forEach(field => { + if (field.scripted !== true) { + const dataVisualizerType: ML_JOB_FIELD_TYPES | undefined = kbnTypeToMLJobType(field); + if ( + dataVisualizerType !== undefined && + !indexedFieldTypes.includes(dataVisualizerType) && + dataVisualizerType !== ML_JOB_FIELD_TYPES.NUMBER + ) { + indexedFieldTypes.push(dataVisualizerType); + } + } + }); + indexedFieldTypes = indexedFieldTypes.sort(); + + const defaults = getDefaultPageState(); + + const [showActionsPanel] = useState( + isFullLicense() && currentIndexPattern.timeFieldName !== undefined + ); + + const [searchString, setSearchString] = useState(defaults.searchString); + const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); + const [searchQueryLanguage, setSearchQueryLanguage] = useState(defaults.searchQueryLanguage); + const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); + + // TODO - type overallStats and stats + const [overallStats, setOverallStats] = useState(defaults.overallStats); + + const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); + const [totalMetricFieldCount, setTotalMetricFieldCount] = useState( + defaults.totalMetricFieldCount + ); + const [populatedMetricFieldCount, setPopulatedMetricFieldCount] = useState( + defaults.populatedMetricFieldCount + ); + const [showAllMetrics, setShowAllMetrics] = useState(defaults.showAllMetrics); + const [metricFieldQuery, setMetricFieldQuery] = useState(defaults.metricFieldQuery); + + const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); + const [totalNonMetricFieldCount, setTotalNonMetricFieldCount] = useState( + defaults.totalNonMetricFieldCount + ); + const [populatedNonMetricFieldCount, setPopulatedNonMetricFieldCount] = useState( + defaults.populatedNonMetricFieldCount + ); + const [showAllNonMetrics, setShowAllNonMetrics] = useState(defaults.showAllNonMetrics); + + const [nonMetricShowFieldType, setNonMetricShowFieldType] = useState( + defaults.nonMetricShowFieldType + ); + + const [nonMetricFieldQuery, setNonMetricFieldQuery] = useState(defaults.nonMetricFieldQuery); + + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getTimeUpdate$(), + mlTimefilterRefresh$ + ).subscribe(loadOverallStats); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }); + + useEffect(() => { + // Check for a saved search being passed in. + const searchSource = currentSavedSearch.searchSource; + const query = searchSource.getField('query'); + if (query !== undefined) { + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; + const qryString = query.query; + let qry; + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { + qry = { + query_string: { + query: qryString, + default_operator: 'AND', + }, + }; + } else { + qry = esQuery.luceneStringToDsl(qryString); + esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + } + + setSearchQuery(qry); + setSearchString(qryString); + setSearchQueryLanguage(queryLanguage); + } + }, []); + + useEffect(() => { + loadOverallStats(); + }, [searchQuery, samplerShardSize]); + + useEffect(() => { + createMetricCards(); + createNonMetricCards(); + }, [overallStats]); + + useEffect(() => { + loadMetricFieldStats(); + }, [metricConfigs]); + + useEffect(() => { + loadNonMetricFieldStats(); + }, [nonMetricConfigs]); + + useEffect(() => { + createMetricCards(); + }, [showAllMetrics, metricFieldQuery]); + + useEffect(() => { + createNonMetricCards(); + }, [showAllNonMetrics, nonMetricShowFieldType, nonMetricFieldQuery]); + + async function loadOverallStats() { + const tf = timefilter as any; + let earliest; + let latest; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + try { + const allStats = await dataLoader.loadOverallData( + searchQuery, + samplerShardSize, + earliest, + latest + ); + setOverallStats(allStats); + } catch (err) { + dataLoader.displayError(err); + } + } + + async function loadMetricFieldStats() { + // Only request data for fields that exist in documents. + if (metricConfigs.length === 0) { + return; + } + + const configsToLoad = metricConfigs.filter( + config => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = new TimeBuckets(); + + const tf = timefilter as any; + let earliest: number | undefined; + let latest: number | undefined; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const BAR_TARGET = 75; + buckets.setInterval('auto'); + buckets.setBounds(bounds); + buckets.setBarTarget(BAR_TARGET); + const aggInterval = buckets.getInterval(); + + try { + const metricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existMetricFields, + aggInterval.expression + ); + + // Add the metric stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + metricConfigs.forEach(config => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...metricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + } else { + // Document count card. + configWithStats.stats = metricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === undefined + ); + + // Add earliest / latest of timefilter for setting x axis domain. + configWithStats.stats.timeRangeEarliest = earliest; + configWithStats.stats.timeRangeLatest = latest; + } + configWithStats.loading = false; + configs.push(configWithStats); + }); + + setMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + async function loadNonMetricFieldStats() { + // Only request data for fields that exist in documents. + if (nonMetricConfigs.length === 0) { + return; + } + + const configsToLoad = nonMetricConfigs.filter( + config => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + const tf = timefilter as any; + let earliest; + let latest; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + try { + const nonMetricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existNonMetricFields + ); + + // Add the field stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + nonMetricConfigs.forEach(config => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...nonMetricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + } + configWithStats.loading = false; + configs.push(configWithStats); + }); + + setNonMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + function createMetricCards() { + const configs: FieldVisConfig[] = []; + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + + let allMetricFields = indexPatternFields.filter(f => { + return ( + f.type === KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true + ); + }); + if (metricFieldQuery !== undefined) { + const metricFieldRegexp = new RegExp(`(${metricFieldQuery})`, 'gi'); + allMetricFields = allMetricFields.filter(f => { + const addField = f.displayName !== undefined && !!f.displayName.match(metricFieldRegexp); + return addField; + }); + } + + const metricExistsFields = allMetricFields.filter(f => { + return aggregatableExistsFields.find(existsF => { + return existsF.fieldName === f.displayName; + }); + }); + + // Add a config for 'document count', identified by no field name if indexpattern is time based. + let allFieldCount = allMetricFields.length; + let popFieldCount = metricExistsFields.length; + if (currentIndexPattern.timeFieldName !== undefined) { + configs.push({ + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + loading: true, + aggregatable: true, + }); + allFieldCount++; + popFieldCount++; + } + + // Add on 1 for the document count card. + setTotalMetricFieldCount(allFieldCount); + setPopulatedMetricFieldCount(popFieldCount); + + if (allMetricFields.length === metricExistsFields.length && showAllMetrics === false) { + setShowAllMetrics(true); + return; + } + + let aggregatableFields: any[] = overallStats.aggregatableExistsFields; + if (allMetricFields.length !== metricExistsFields.length && showAllMetrics === true) { + aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); + } + + const metricFieldsToShow = showAllMetrics === true ? allMetricFields : metricExistsFields; + + metricFieldsToShow.forEach(field => { + const fieldData = aggregatableFields.find(f => { + return f.fieldName === field.displayName; + }); + + if (fieldData !== undefined) { + const metricConfig: FieldVisConfig = { + ...fieldData, + fieldFormat: field.format, + type: ML_JOB_FIELD_TYPES.NUMBER, + loading: true, + aggregatable: true, + }; + + configs.push(metricConfig); + } + }); + + setMetricConfigs(configs); + } + + function createNonMetricCards() { + let allNonMetricFields = []; + if (nonMetricShowFieldType === '*') { + allNonMetricFields = indexPatternFields.filter(f => { + return ( + f.type !== KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true + ); + }); + } else { + if ( + nonMetricShowFieldType === ML_JOB_FIELD_TYPES.TEXT || + nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD + ) { + const aggregatableCheck = + nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD ? true : false; + allNonMetricFields = indexPatternFields.filter(f => { + return ( + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true && + f.type === KBN_FIELD_TYPES.STRING && + f.aggregatable === aggregatableCheck + ); + }); + } else { + allNonMetricFields = indexPatternFields.filter(f => { + return ( + f.type === nonMetricShowFieldType && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true + ); + }); + } + } + + // If a field filter has been entered, perform another filter on the entered regexp. + if (nonMetricFieldQuery !== undefined) { + const nonMetricFieldRegexp = new RegExp(`(${nonMetricFieldQuery})`, 'gi'); + allNonMetricFields = allNonMetricFields.filter( + f => f.displayName !== undefined && f.displayName.match(nonMetricFieldRegexp) + ); + } + + // Obtain the list of all non-metric fields which appear in documents + // (aggregatable or not aggregatable). + const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. + let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; + + allNonMetricFields.forEach(f => { + const checkAggregatableField = aggregatableExistsFields.find( + existsField => existsField.fieldName === f.displayName + ); + + if (checkAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkAggregatableField); + } else { + const checkNonAggregatableField = nonAggregatableExistsFields.find( + existsField => existsField.fieldName === f.displayName + ); + + if (checkNonAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkNonAggregatableField); + } + } + }); + + setTotalNonMetricFieldCount(allNonMetricFields.length); + setPopulatedNonMetricFieldCount(nonMetricFieldData.length); + + if (allNonMetricFields.length === nonMetricFieldData.length && showAllNonMetrics === false) { + setShowAllNonMetrics(true); + return; + } + + if (allNonMetricFields.length !== nonMetricFieldData.length && showAllNonMetrics === true) { + // Combine the field data obtained from Elasticsearch into a single array. + nonMetricFieldData = nonMetricFieldData.concat( + overallStats.aggregatableNotExistsFields, + overallStats.nonAggregatableNotExistsFields + ); + } + + const nonMetricFieldsToShow = + showAllNonMetrics === true ? allNonMetricFields : populatedNonMetricFields; + + const configs: FieldVisConfig[] = []; + + nonMetricFieldsToShow.forEach(field => { + const fieldData = nonMetricFieldData.find(f => f.fieldName === field.displayName); + + const nonMetricConfig = { + ...fieldData, + fieldFormat: field.format, + aggregatable: field.aggregatable, + scripted: field.scripted, + loading: fieldData.existsInDocs, + }; + + // Map the field type from the Kibana index pattern to the field type + // used in the data visualizer. + const dataVisualizerType = kbnTypeToMLJobType(field); + if (dataVisualizerType !== undefined) { + nonMetricConfig.type = dataVisualizerType; + } else { + // Add a flag to indicate that this is one of the 'other' Kibana + // field types that do not yet have a specific card type. + nonMetricConfig.type = field.type; + nonMetricConfig.isUnsupportedType = true; + } + + configs.push(nonMetricConfig); + }); + + setNonMetricConfigs(configs); + } + + return ( + + + + + + + +

{currentIndexPattern.title}

+
+
+ {currentIndexPattern.timeFieldName !== undefined && ( + + + + )} +
+ + + + + + + + + {totalMetricFieldCount > 0 && ( + + + + + )} + + + + + {showActionsPanel === true && ( + + + + )} + + +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_anomalies_table_data.json b/x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_anomalies_table_data.json rename to x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json b/x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_overall_swimlane.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json rename to x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_overall_swimlane.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/__tests__/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__tests__/explorer_controller.js rename to x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/_explorer.scss b/x-pack/legacy/plugins/ml/public/application/explorer/_explorer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/_explorer.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/_explorer.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js new file mode 100644 index 0000000000000..243adecaec78f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; +import { i18n } from '@kbn/i18n'; + + +export function getAnomalyExplorerBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer' + }), + href: '' + } + ]; +} + diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/explorer/explorer.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 1acdd041c4052..985282df18f6a 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -41,7 +41,7 @@ import { getBoundsRoundedToInterval } from '../util/time_buckets'; import { getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service'; -import { mlResultsService } from 'plugins/ml/services/results_service'; +import { mlResultsService } from '../services/results_service'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { NavigationMenu } from '../components/navigation_menu'; import { CheckboxShowCharts, showCharts$ } from '../components/controls/checkbox_showcharts'; @@ -89,7 +89,7 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; // Explorer Charts import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_container'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_record.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_record.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_job_config.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_job_config.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_rare.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_rare.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_rare.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_rare.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_promises_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_promises_response.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_charts_container.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_charts_container.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js similarity index 94% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js index e62707d60a2a1..6d89d2de44498 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js @@ -13,8 +13,8 @@ import _ from 'lodash'; -import { parseInterval } from '../../../common/util/parse_interval'; -import { getEntityFieldList } from '../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; +import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; import { buildConfigFromDetector } from '../../util/chart_config_builder'; import { mlJobService } from '../../services/job_service'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 588c3e3d6f1e9..1544e3a866001 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -21,7 +21,7 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; -import { getSeverityColor, getSeverityWithLow } from '../../../common/util/anomaly_utils'; +import { getSeverityColor, getSeverityWithLow } from '../../../../common/util/anomaly_utils'; import { getChartType, getTickValues, diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index be85af5a70c40..963da9e1d5efa 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -25,7 +25,7 @@ import { getSeverityColor, getSeverityWithLow, getMultiBucketImpactLabel, -} from '../../../common/util/anomaly_utils'; +} from '../../../../common/util/anomaly_utils'; import { LINE_CHART_ANOMALY_RADIUS, MULTI_BUCKET_SYMBOL_SIZE, diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 9d7e6e81e4896..01afd9ffb602f 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -19,8 +19,8 @@ import { getChartType } from '../../util/chart_utils'; -import { getEntityFieldList } from '../../../common/util/anomaly_utils'; -import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../common/util/job_utils'; +import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; +import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; import { severity$ } from '../../components/controls/select_severity/select_severity'; @@ -124,7 +124,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, config.interval - ); + ).toPromise(); } else { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; @@ -169,7 +169,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, interval - ) + ).toPromise() .then((resp) => { // Return data in format required by the explorer charts. const results = resp.results; @@ -201,7 +201,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, ANOMALIES_MAX_RESULTS - ); + ).toPromise(); } // Query 3 - load any scheduled events for the job. @@ -213,7 +213,7 @@ export function explorerChartsContainerServiceFactory(callback) { config.interval, 1, MAX_SCHEDULED_EVENTS - ); + ).toPromise(); } // Query 4 - load context data distribution diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js similarity index 86% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index b907cd92df10c..f8ed067a3de54 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -47,36 +47,39 @@ jest.mock('../../services/job_service', () => ({ } })); -jest.mock('../../services/results_service', () => ({ - mlResultsService: { - getMetricData(indices) { +jest.mock('../../services/results_service', () => { + const { of } = require('rxjs'); + return { + mlResultsService: { + getMetricData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve(mockSeriesPromisesResponse[0][0]); - } - // this is for 'filtering should skip values of null' - return Promise.resolve(mockMetricClone); - }, - getRecordsForCriteria() { - return Promise.resolve(mockSeriesPromisesResponse[0][1]); - }, - getScheduledEventsByBucket() { - return Promise.resolve(mockSeriesPromisesResponse[0][2]); - }, - getEventDistributionData(indices) { + if (indices[0] === 'farequote-2017') { + return of(mockSeriesPromisesResponse[0][0]); + } + // this is for 'filtering should skip values of null' + return of(mockMetricClone); + }, + getRecordsForCriteria() { + return of(mockSeriesPromisesResponse[0][1]); + }, + getScheduledEventsByBucket() { + return of(mockSeriesPromisesResponse[0][2]); + }, + getEventDistributionData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve([]); + if (indices[0] === 'farequote-2017') { + return Promise.resolve([]); + } + // this is for 'filtering should skip values of null' and + // resolves with a dummy object to trigger the processing + // of the event distribution chartdata filtering + return Promise.resolve([{ + entity: 'mock' + }]); } - // this is for 'filtering should skip values of null' and - // resolves with a dummy object to trigger the processing - // of the event distribution chartdata filtering - return Promise.resolve([{ - entity: 'mock' - }]); } - } -})); + }; +}); jest.mock('../../util/string_utils', () => ({ mlEscape(d) { return d; } diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js new file mode 100644 index 0000000000000..6d79975818870 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_constants.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js index 3bedd90f05c37..c33b86bacf942 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js @@ -24,10 +24,10 @@ import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { loadIndexPatterns } from '../util/index_utils'; -import { TimeBuckets } from 'plugins/ml/util/time_buckets'; +import { TimeBuckets } from '../util/time_buckets'; import { explorer$ } from './explorer_dashboard_service'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; -import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; +import { mlFieldFormatService } from '../services/field_format_service'; import { mlJobService } from '../services/job_service'; import { getSelectedJobIds, jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; import { timefilter } from 'ui/timefilter'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_dashboard_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_dashboard_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js index 2ee725b6fda86..bd35241ff4e85 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -21,7 +21,7 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatHumanReadableDateTime } from '../util/date_utils'; import { numTicksForDateFormat } from '../util/chart_utils'; -import { getSeverityColor } from '../../common/util/anomaly_utils'; +import { getSeverityColor } from '../../../common/util/anomaly_utils'; import { mlEscape } from '../util/string_utils'; import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_utils.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 2f8b259b594bb..5ca8681d16749 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -10,12 +10,12 @@ import { chain, each, get, union, uniq } from 'lodash'; -import { getEntityFieldList } from '../../common/util/anomaly_utils'; -import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; +import { getEntityFieldList } from '../../../common/util/anomaly_utils'; +import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../common/util/job_utils'; +import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; -import { mlResultsService } from 'plugins/ml/services/results_service'; +import { mlResultsService } from '../services/results_service'; import { MAX_CATEGORY_EXAMPLES, @@ -26,7 +26,7 @@ import { import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../common/constants/search'; +} from '../../../common/constants/search'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; @@ -417,7 +417,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { if (resp.error !== undefined || resp.annotations === undefined) { return resolve([]); } @@ -477,7 +477,7 @@ export async function loadAnomaliesTableData( ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, MAX_CATEGORY_EXAMPLES, influencersFilterQuery - ).then((resp) => { + ).toPromise().then((resp) => { const anomalies = resp.anomalies; const detectorsByJob = mlJobService.detectorsByJob; anomalies.forEach((anomaly) => { diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/index.js new file mode 100644 index 0000000000000..ebd3eb9c12662 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/index.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import '../explorer/explorer_controller'; +import '../explorer/explorer_dashboard_service'; +import '../explorer/explorer_react_wrapper_directive'; +import '../explorer/explorer_charts'; +import '../explorer/select_limit'; +import '../components/job_selector'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/legacy_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/legacy_utils.js rename to x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit_service.js diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.test.ts new file mode 100644 index 0000000000000..feabaa4064978 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { abbreviateWholeNumber } from './abbreviate_whole_number'; + +describe('ML - abbreviateWholeNumber formatter', () => { + test('returns the correct format using default max digits', () => { + expect(abbreviateWholeNumber(1)).toBe(1); + expect(abbreviateWholeNumber(12)).toBe(12); + expect(abbreviateWholeNumber(123)).toBe(123); + expect(abbreviateWholeNumber(1234)).toBe('1k'); + expect(abbreviateWholeNumber(12345)).toBe('12k'); + expect(abbreviateWholeNumber(123456)).toBe('123k'); + expect(abbreviateWholeNumber(1234567)).toBe('1m'); + expect(abbreviateWholeNumber(12345678)).toBe('12m'); + expect(abbreviateWholeNumber(123456789)).toBe('123m'); + expect(abbreviateWholeNumber(1234567890)).toBe('1b'); + expect(abbreviateWholeNumber(5555555555555.55)).toBe('6t'); + }); + + test('returns the correct format using custom max digits', () => { + expect(abbreviateWholeNumber(1, 4)).toBe(1); + expect(abbreviateWholeNumber(12, 4)).toBe(12); + expect(abbreviateWholeNumber(123, 4)).toBe(123); + expect(abbreviateWholeNumber(1234, 4)).toBe(1234); + expect(abbreviateWholeNumber(12345, 4)).toBe('12k'); + expect(abbreviateWholeNumber(123456, 6)).toBe(123456); + expect(abbreviateWholeNumber(1234567, 4)).toBe('1m'); + expect(abbreviateWholeNumber(12345678, 3)).toBe('12m'); + expect(abbreviateWholeNumber(123456789, 9)).toBe(123456789); + expect(abbreviateWholeNumber(1234567890, 3)).toBe('1b'); + expect(abbreviateWholeNumber(5555555555555.55, 5)).toBe('6t'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.ts b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.ts new file mode 100644 index 0000000000000..6d630c740a359 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Formatter to abbreviate large whole numbers with metric prefixes. + * Uses numeral.js to format numbers longer than the specified number of + * digits with metric abbreviations e.g. 12345 as 12k, or 98000000 as 98m. + */ +import numeral from '@elastic/numeral'; + +export function abbreviateWholeNumber(value: number, maxDigits = 3) { + if (Math.abs(value) < Math.pow(10, maxDigits)) { + return value; + } else { + return numeral(value).format('0a'); + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts new file mode 100644 index 0000000000000..bfed06a537a87 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 moment from 'moment-timezone'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +import { formatValue } from './format_value'; + +describe('ML - formatValue formatter', () => { + const timeOfWeekRecord: AnomalyRecordDoc = { + job_id: 'gallery_time_of_week', + result_type: 'record', + probability: 0.012818, + record_score: 53.55134, + initial_record_score: 53, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1530155700000, + by_field_name: 'clientip', + by_field_value: '65.55.215.39', + function: 'time_of_week', + function_description: 'time', + }; + + const timeOfDayRecord: AnomalyRecordDoc = { + job_id: 'gallery_time_of_day', + result_type: 'record', + probability: 0.012818, + record_score: 97.94245, + initial_record_score: 97, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1517472900000, + by_field_name: 'clientip', + by_field_value: '157.56.93.83', + function: 'time_of_day', + function_description: 'time', + }; + + // Set timezone to US/Eastern for time_of_day and time_of_week tests. + beforeEach(() => { + moment.tz.setDefault('US/Eastern'); + }); + + afterEach(() => { + moment.tz.setDefault('Browser'); + }); + + // For time_of_day and time_of_week test values which are offsets in seconds + // from UTC start of week / day are formatted correctly using the test timezone. + test('correctly formats time_of_week value from numeric input', () => { + expect(formatValue(359739, 'time_of_week', undefined, timeOfWeekRecord)).toBe('Wed 23:55'); + }); + + test('correctly formats time_of_day value from numeric input', () => { + expect(formatValue(73781, 'time_of_day', undefined, timeOfDayRecord)).toBe('15:29'); + }); + + test('correctly formats number values from numeric input', () => { + expect(formatValue(1483228800, 'mean')).toBe(1483228800); + expect(formatValue(1234.5678, 'mean')).toBe(1234.6); + expect(formatValue(0.00012345, 'mean')).toBe(0.000123); + expect(formatValue(0, 'mean')).toBe(0); + expect(formatValue(-0.12345, 'mean')).toBe(-0.123); + expect(formatValue(-1234.5678, 'mean')).toBe(-1234.6); + expect(formatValue(-100000.1, 'mean')).toBe(-100000); + }); + + test('correctly formats time_of_week value from array input', () => { + expect(formatValue([359739], 'time_of_week', undefined, timeOfWeekRecord)).toBe('Wed 23:55'); + }); + + test('correctly formats time_of_day value from array input', () => { + expect(formatValue([73781], 'time_of_day', undefined, timeOfDayRecord)).toBe('15:29'); + }); + + test('correctly formats number values from array input', () => { + expect(formatValue([1483228800], 'mean')).toBe(1483228800); + expect(formatValue([1234.5678], 'mean')).toBe(1234.6); + expect(formatValue([0.00012345], 'mean')).toBe(0.000123); + expect(formatValue([0], 'mean')).toBe(0); + expect(formatValue([-0.12345], 'mean')).toBe(-0.123); + expect(formatValue([-1234.5678], 'mean')).toBe(-1234.6); + expect(formatValue([-100000.1], 'mean')).toBe(-100000); + }); + + test('correctly formats multi-valued array', () => { + expect(formatValue([30.3, 26.2], 'lat_long')).toBe('[30.3,26.2]'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts new file mode 100644 index 0000000000000..abafe65615156 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Formatter for 'typical' and 'actual' values from machine learning results. + * For detectors which use the time_of_week or time_of_day + * functions, the filter converts the raw number, which is the number of seconds since + * midnight, into a human-readable date/time format. + */ + +import moment from 'moment'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +const SIGFIGS_IF_ROUNDING = 3; // Number of sigfigs to use for values < 10 + +// Formats the value of an actual or typical field from a machine learning anomaly record. +// mlFunction is the 'function' field from the ML record containing what the user entered e.g. 'high_count', +// (as opposed to the 'function_description' field which holds an ML-built display hint for the function e.g. 'count'. +// If a Kibana fieldFormat is not supplied, will fall back to default +// formatting depending on the magnitude of the value. +// For time_of_day or time_of_week functions the anomaly record +// containing the timestamp of the anomaly should be supplied in +// order to correctly format the day or week offset to the time of the anomaly. +export function formatValue( + value: number[] | number, + mlFunction: string, + fieldFormat?: any, + record?: AnomalyRecordDoc +) { + // actual and typical values in anomaly record results will be arrays. + // Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long), + // simply return the formatted single value. + if (Array.isArray(value)) { + if (value.length === 1) { + return formatSingleValue(value[0], mlFunction, fieldFormat, record); + } else { + // Currently only multi-value response is for lat_long detectors. + // Return with array style formatting, with items formatted as numbers, rather than + // the default String format which is set for geo_point and geo_shape fields. + const values = value.map(val => formatSingleValue(val, mlFunction, undefined, record)); + return `[${values}]`; + } + } else { + return formatSingleValue(value, mlFunction, fieldFormat, record); + } +} + +// Formats a single value according to the specified ML function. +// If a Kibana fieldFormat is not supplied, will fall back to default +// formatting depending on the magnitude of the value. +// For time_of_day or time_of_week functions the anomaly record +// containing the timestamp of the anomaly should be supplied in +// order to correctly format the day or week offset to the time of the anomaly. +function formatSingleValue( + value: number, + mlFunction: string, + fieldFormat?: any, + record?: AnomalyRecordDoc +) { + if (value === undefined || value === null) { + return ''; + } + + // If the analysis function is time_of_week/day, format as day/time. + // For time_of_week / day, actual / typical is the UTC offset in seconds from the + // start of the week / day, so need to manipulate to UTC moment of the start of the week / day + // that the anomaly occurred using record timestamp if supplied, add on the offset, and finally + // revert back to configured timezone for formatting. + if (mlFunction === 'time_of_week') { + const d = + record !== undefined && record.timestamp !== undefined + ? new Date(record.timestamp) + : new Date(); + const utcMoment = moment + .utc(d) + .startOf('week') + .add(value, 's'); + return moment(utcMoment.valueOf()).format('ddd HH:mm'); + } else if (mlFunction === 'time_of_day') { + const d = + record !== undefined && record.timestamp !== undefined + ? new Date(record.timestamp) + : new Date(); + const utcMoment = moment + .utc(d) + .startOf('day') + .add(value, 's'); + return moment(utcMoment.valueOf()).format('HH:mm'); + } else { + if (fieldFormat !== undefined) { + return fieldFormat.convert(value, 'text'); + } else { + // If no Kibana FieldFormat object provided, + // format the value depending on its magnitude. + const absValue = Math.abs(value); + if (absValue >= 10000 || absValue === Math.floor(absValue)) { + // Output 0 decimal places if whole numbers or >= 10000 + if (fieldFormat !== undefined) { + return fieldFormat.convert(value, 'text'); + } else { + return Number(value.toFixed(0)); + } + } else if (absValue >= 10) { + // Output to 1 decimal place between 10 and 10000 + return Number(value.toFixed(1)); + } else { + // For values < 10, output to 3 significant figures + let multiple; + if (value > 0) { + multiple = Math.pow( + 10, + SIGFIGS_IF_ROUNDING - Math.floor(Math.log(value) / Math.LN10) - 1 + ); + } else { + multiple = Math.pow( + 10, + SIGFIGS_IF_ROUNDING - Math.floor(Math.log(-1 * value) / Math.LN10) - 1 + ); + } + return Math.round(value * multiple) / multiple; + } + } + } +} diff --git a/x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts b/x-pack/legacy/plugins/ml/public/application/formatters/kibana_field_format.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/kibana_field_format.ts diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.test.ts new file mode 100644 index 0000000000000..93533fe155e80 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMetricChangeDescription } from './metric_change_description'; + +describe('ML - metricChangeDescription formatter', () => { + test('returns correct icon and message if actual > typical', () => { + expect(getMetricChangeDescription(1.01, 1)).toEqual({ + iconType: 'sortUp', + message: 'Unusually high', + }); + expect(getMetricChangeDescription(1.123, 1)).toEqual({ + iconType: 'sortUp', + message: '1.1x higher', + }); + expect(getMetricChangeDescription(2, 1)).toEqual({ iconType: 'sortUp', message: '2x higher' }); + expect(getMetricChangeDescription(9.5, 1)).toEqual({ + iconType: 'sortUp', + message: '10x higher', + }); + expect(getMetricChangeDescription(1000, 1)).toEqual({ + iconType: 'sortUp', + message: 'More than 100x higher', + }); + expect(getMetricChangeDescription(1, 0)).toEqual({ + iconType: 'sortUp', + message: 'Unexpected non-zero value', + }); + }); + + test('returns correct icon and message if actual < typical', () => { + expect(getMetricChangeDescription(1, 1.01)).toEqual({ + iconType: 'sortDown', + message: 'Unusually low', + }); + expect(getMetricChangeDescription(1, 1.123)).toEqual({ + iconType: 'sortDown', + message: '1.1x lower', + }); + expect(getMetricChangeDescription(1, 2)).toEqual({ iconType: 'sortDown', message: '2x lower' }); + expect(getMetricChangeDescription(1, 9.5)).toEqual({ + iconType: 'sortDown', + message: '10x lower', + }); + expect(getMetricChangeDescription(1, 1000)).toEqual({ + iconType: 'sortDown', + message: 'More than 100x lower', + }); + expect(getMetricChangeDescription(0, 1)).toEqual({ + iconType: 'sortDown', + message: 'Unexpected zero value', + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.ts b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.ts new file mode 100644 index 0000000000000..68f437b5a1436 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Produces a concise textual description of how the + * actual value compares to the typical value for an anomaly. + */ + +import { i18n } from '@kbn/i18n'; + +// Returns an Object containing a text message and EuiIcon type to +// describe how the actual value compares to the typical. +export function getMetricChangeDescription( + actualProp: number[] | number, + typicalProp: number[] | number +) { + if (actualProp === undefined || typicalProp === undefined) { + return { iconType: 'empty', message: '' }; + } + + let iconType: string = 'alert'; + let message: string; + + // For metric functions, actual and typical will be single value arrays. + let actual: number = 0; + let typical: number = 0; + if (Array.isArray(actualProp)) { + if (actualProp.length === 1) { + actual = actualProp[0]; + } else { + // lat_long anomalies currently the only multi-value case. + // TODO - do we want to enhance the description depending on detector? + // e.g. 'Unusual location' if using a lat_long detector. + return { + iconType: 'alert', + message: i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.unusualValuesDescription', + { + defaultMessage: 'Unusual values', + } + ), + }; + } + } else { + actual = actualProp; + } + + if (Array.isArray(typicalProp)) { + if (typicalProp.length === 1) { + typical = typicalProp[0]; + } + } else { + typical = typicalProp; + } + + if (actual === typical) { + // Very unlikely, but just in case. + message = i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription', + { + defaultMessage: 'actual same as typical', + } + ); + } else { + // For actual / typical gives output of the form: + // 4 / 2 2x higher + // 2 / 10 5x lower + // 1000 / 1 More than 100x higher + // 999 / 1000 Unusually low + // 100 / -100 Unusually high + // 0 / 100 Unexpected zero value + // 1 / 0 Unexpected non-zero value + const isHigher = actual > typical; + iconType = isHigher ? 'sortUp' : 'sortDown'; + if (typical !== 0 && actual !== 0) { + const factor: number = isHigher ? actual / typical : typical / actual; + if (factor > 1.5) { + if (factor <= 100) { + message = isHigher + ? i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThanOneAndHalfxHigherDescription', + { + defaultMessage: '{factor}x higher', + values: { factor: Math.round(factor) }, + } + ) + : i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThanOneAndHalfxLowerDescription', + { + defaultMessage: '{factor}x lower', + values: { factor: Math.round(factor) }, + } + ); + } else { + message = isHigher + ? i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription', + { + defaultMessage: 'More than 100x higher', + } + ) + : i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription', + { + defaultMessage: 'More than 100x lower', + } + ); + } + } else if (factor >= 1.05) { + message = isHigher + ? i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThanOneAndFiveHundredthsxHigherDescription', + { + defaultMessage: '{factor}x higher', + values: { factor: factor.toPrecision(2) }, + } + ) + : i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.moreThanOneAndFiveHundredthsxLowerDescription', + { + defaultMessage: '{factor}x lower', + values: { factor: factor.toPrecision(2) }, + } + ); + } else { + message = isHigher + ? i18n.translate('xpack.ml.formatters.metricChangeDescription.unusuallyHighDescription', { + defaultMessage: 'Unusually high', + }) + : i18n.translate('xpack.ml.formatters.metricChangeDescription.unusuallyLowDescription', { + defaultMessage: 'Unusually low', + }); + } + } else { + if (actual === 0) { + message = i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.unexpectedZeroValueDescription', + { + defaultMessage: 'Unexpected zero value', + } + ); + } else { + message = i18n.translate( + 'xpack.ml.formatters.metricChangeDescription.unexpectedNonZeroValueDescription', + { + defaultMessage: 'Unexpected non-zero value', + } + ); + } + } + } + + return { iconType, message }; +} diff --git a/x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts diff --git a/x-pack/legacy/plugins/ml/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js similarity index 90% rename from x-pack/legacy/plugins/ml/public/hacks/toggle_app_link_in_nav.js rename to x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js index 892d530c32735..7c6d8345736b5 100644 --- a/x-pack/legacy/plugins/ml/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { uiModules } from 'ui/modules'; import { npStart } from 'ui/new_platform'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts new file mode 100644 index 0000000000000..f2954548ea547 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Breadcrumb } from 'ui/chrome'; +import { + ANOMALY_DETECTION_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + ML_BREADCRUMB, +} from '../../breadcrumbs'; + +export function getJobManagementBreadcrumbs(): Breadcrumb[] { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ]; +} + +export function getCreateJobBreadcrumbs(): Breadcrumb[] { + return [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { + defaultMessage: 'Create job', + }), + href: '#/jobs/new_job', + }, + ]; +} + +export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { + return [ + ...getCreateJobBreadcrumbs(), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { + defaultMessage: 'Single metric', + }), + href: '', + }, + ]; +} + +export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { + return [ + ...getCreateJobBreadcrumbs(), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { + defaultMessage: 'Multi metric', + }), + href: '', + }, + ]; +} + +export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { + return [ + ...getCreateJobBreadcrumbs(), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { + defaultMessage: 'Population', + }), + href: '', + }, + ]; +} + +export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { + return [ + ...getCreateJobBreadcrumbs(), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { + defaultMessage: 'Advanced configuration', + }), + href: '', + }, + ]; +} + +export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { + return [ + ...getCreateJobBreadcrumbs(), + { + text: $routeParams.id, + href: '', + }, + ]; +} + +export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { + return [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { + defaultMessage: 'Select index or search', + }), + href: '', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_custom_url_editor.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_custom_url_editor.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/constants.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/constants.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js index 6edee63a07132..bbf3c3cfbadce 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js @@ -28,7 +28,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { isValidCustomUrlSettingsTimeRange } from '../../../jobs/components/custom_url_editor/utils'; +import { isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.test.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.test.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.test.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.test.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.test.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx new file mode 100644 index 0000000000000..ffb552da8ecf3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, useState, ChangeEvent } from 'react'; + +import { + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiToolTip, + EuiTextArea, +} from '@elastic/eui'; + +import { toastNotifications } from 'ui/notify'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; +import { getTestUrl } from './utils'; + +import { parseInterval } from '../../../../../common/util/parse_interval'; +import { TIME_RANGE_TYPE } from './constants'; +import { KibanaUrlConfig } from '../../../../../common/types/custom_urls'; +import { Job } from '../../new_job/common/job_creator/configs'; + +function isValidTimeRange(timeRange: KibanaUrlConfig['time_range']): boolean { + // Allow empty timeRange string, which gives the 'auto' behaviour. + if (timeRange === undefined || timeRange.length === 0 || timeRange === TIME_RANGE_TYPE.AUTO) { + return true; + } + + const interval = parseInterval(timeRange); + return interval !== null; +} + +export interface CustomUrlListProps { + job: Job; + customUrls: KibanaUrlConfig[]; + setCustomUrls: (customUrls: KibanaUrlConfig[]) => {}; +} + +/* + * React component for listing the custom URLs added to a job, + * with buttons for testing and deleting each custom URL. + */ +export const CustomUrlList: FC = ({ job, customUrls, setCustomUrls }) => { + const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); + + const onLabelChange = (e: ChangeEvent, index: number) => { + if (index < customUrls.length) { + customUrls[index] = { + ...customUrls[index], + url_name: e.target.value, + }; + setCustomUrls(customUrls); + } + }; + + const onUrlValueChange = ( + e: ChangeEvent, + index: number + ) => { + if (index < customUrls.length) { + customUrls[index] = { + ...customUrls[index], + url_value: e.target.value, + }; + setCustomUrls(customUrls); + } + }; + + const onTimeRangeChange = (e: ChangeEvent, index: number) => { + if (index < customUrls.length) { + customUrls[index] = { + ...customUrls[index], + }; + + const timeRange = e.target.value; + if (timeRange !== undefined && timeRange.length > 0) { + customUrls[index].time_range = timeRange; + } else { + delete customUrls[index].time_range; + } + setCustomUrls(customUrls); + } + }; + + const onDeleteButtonClick = (index: number) => { + if (index < customUrls.length) { + customUrls.splice(index, 1); + setCustomUrls(customUrls); + } + }; + + const onTestButtonClick = (index: number) => { + if (index < customUrls.length) { + getTestUrl(job, customUrls[index]) + .then(testUrl => { + openCustomUrlWindow(testUrl, customUrls[index]); + }) + .catch(resp => { + // eslint-disable-next-line no-console + console.error('Error obtaining URL for test:', resp); + toastNotifications.addDanger( + i18n.translate( + 'xpack.ml.customUrlEditorList.obtainingUrlToTestConfigurationErrorMessage', + { + defaultMessage: 'An error occurred obtaining the URL to test the configuration', + } + ) + ); + }); + } + }; + + const customUrlRows = customUrls.map((customUrl, index) => { + // Validate the label. + const label = customUrl.url_name; + const otherUrls = [...customUrls]; + otherUrls.splice(index, 1); // Don't compare label with itself. + const isInvalidLabel = !isValidLabel(label, otherUrls); + const invalidLabelError = isInvalidLabel + ? [ + i18n.translate('xpack.ml.customUrlEditorList.labelIsNotUniqueErrorMessage', { + defaultMessage: 'A unique label must be supplied', + }), + ] + : []; + + // Validate the time range. + const timeRange = customUrl.time_range; + const isInvalidTimeRange = !isValidTimeRange(timeRange); + const invalidIntervalError = isInvalidTimeRange + ? [ + i18n.translate('xpack.ml.customUrlEditorList.invalidTimeRangeFormatErrorMessage', { + defaultMessage: 'Invalid format', + }), + ] + : []; + + return ( + + + + } + isInvalid={isInvalidLabel} + error={invalidLabelError} + > + onLabelChange(e, index)} + /> + + + + + } + > + {index === expandedUrlIndex ? ( + { + if (input) { + input.focus(); + } + }} + fullWidth={true} + value={customUrl.url_value} + onChange={e => onUrlValueChange(e, index)} + onBlur={() => { + setExpandedUrlIndex(null); + }} + data-test-subj={`mlJobEditCustomUrlTextarea_${index}`} + /> + ) : ( + setExpandedUrlIndex(index)} + data-test-subj={`mlJobEditCustomUrlInput_${index}`} + /> + )} + + + + + } + error={invalidIntervalError} + isInvalid={isInvalidTimeRange} + > + onTimeRangeChange(e, index)} + /> + + + + + + } + > + onTestButtonClick(index)} + iconType="popout" + aria-label={i18n.translate('xpack.ml.customUrlEditorList.testCustomUrlAriaLabel', { + defaultMessage: 'Test custom URL', + })} + /> + + + + + + + } + > + onDeleteButtonClick(index)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.ml.customUrlEditorList.deleteCustomUrlAriaLabel', + { + defaultMessage: 'Delete custom URL', + } + )} + /> + + + + + ); + }); + + return <>{customUrlRows}; +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts similarity index 83% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.d.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts index f8f618ae06762..ee9312aace119 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaUrlConfig } from '../../../../common/types/custom_urls'; +import { KibanaUrlConfig } from '../../../../../common/types/custom_urls'; import { Job } from '../../new_job/common/job_creator/configs'; export function getTestUrl(job: Job, customUrl: KibanaUrlConfig): Promise; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js new file mode 100644 index 0000000000000..06391ad7895cb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -0,0 +1,374 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + TIME_RANGE_TYPE, + URL_TYPE +} from './constants'; + +import chrome from 'ui/chrome'; +import rison from 'rison-node'; + +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; +import { parseInterval } from '../../../../../common/util/parse_interval'; +import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; +import { ml } from '../../../services/ml_api_service'; +import { mlJobService } from '../../../services/job_service'; +import { escapeForElasticsearchQuery } from '../../../util/string_utils'; + + +export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { + // Returns the settings object in the format used by the custom URL editor + // for a new custom URL. + const kibanaSettings = { + queryFieldNames: [] + }; + + // Set the default type. + let urlType = URL_TYPE.OTHER; + if (dashboards !== undefined && dashboards.length > 0) { + urlType = URL_TYPE.KIBANA_DASHBOARD; + kibanaSettings.dashboardId = dashboards[0].id; + } else if (indexPatterns !== undefined && indexPatterns.length > 0) { + urlType = URL_TYPE.KIBANA_DISCOVER; + } + + // For the Discover option, set the default index pattern to that + // which matches the (first) index configured in the job datafeed. + const datafeedConfig = job.datafeed_config; + if (indexPatterns !== undefined && indexPatterns.length > 0 && + datafeedConfig !== undefined && + datafeedConfig.indices !== undefined && + datafeedConfig.indices.length > 0) { + + const datafeedIndex = datafeedConfig.indices[0]; + let defaultIndexPattern = indexPatterns.find((indexPattern) => { + return indexPattern.title === datafeedIndex; + }); + + if (defaultIndexPattern === undefined) { + defaultIndexPattern = indexPatterns[0]; + } + + kibanaSettings.discoverIndexPatternId = defaultIndexPattern.id; + } + + return { + label: '', + type: urlType, + // Note timeRange is only editable in new URLs for Dashboard and Discover URLs, + // as for other URLs we have no way of knowing how the field will be used in the URL. + timeRange: { + type: TIME_RANGE_TYPE.AUTO, + interval: '' + }, + kibanaSettings, + otherUrlSettings: { + urlValue: '' + } + }; +} + +export function getQueryEntityFieldNames(job) { + // Returns the list of partitioning and influencer field names that can be used + // as entities to add to the query used when linking to a Kibana dashboard or Discover. + const influencers = job.analysis_config.influencers; + const detectors = job.analysis_config.detectors; + const entityFieldNames = []; + if (influencers !== undefined) { + entityFieldNames.push(...influencers); + } + + detectors.forEach((detector, detectorIndex) => { + const partitioningFields = getPartitioningFieldNames(job, detectorIndex); + + partitioningFields.forEach((fieldName) => { + if (entityFieldNames.indexOf(fieldName) === -1) { + entityFieldNames.push(fieldName); + } + }); + }); + + return entityFieldNames; +} + +export function isValidCustomUrlSettingsTimeRange(timeRangeSettings) { + if (timeRangeSettings.type === TIME_RANGE_TYPE.INTERVAL) { + const interval = parseInterval(timeRangeSettings.interval); + return (interval !== null); + } + + return true; +} + +export function isValidCustomUrlSettings(settings, savedCustomUrls) { + let isValid = isValidLabel(settings.label, savedCustomUrls); + if (isValid === true) { + isValid = isValidCustomUrlSettingsTimeRange(settings.timeRange); + } + return isValid; +} + +export function buildCustomUrlFromSettings(settings) { + // Dashboard URL returns a Promise as a query is made to obtain the full dashboard config. + // So wrap the other two return types in a Promise for consistent return type. + if (settings.type === URL_TYPE.KIBANA_DASHBOARD) { + return buildDashboardUrlFromSettings(settings); + } else if (settings.type === URL_TYPE.KIBANA_DISCOVER) { + return Promise.resolve(buildDiscoverUrlFromSettings(settings)); + } else { + const urlToAdd = { + url_name: settings.label, + url_value: settings.otherUrlSettings.urlValue + }; + + return Promise.resolve(urlToAdd); + } + +} + +function buildDashboardUrlFromSettings(settings) { + // Get the complete list of attributes for the selected dashboard (query, filters). + return new Promise((resolve, reject) => { + const { dashboardId, queryFieldNames } = settings.kibanaSettings; + + const savedObjectsClient = chrome.getSavedObjectsClient(); + savedObjectsClient.get('dashboard', dashboardId) + .then((response) => { + // Use the filters from the saved dashboard if there are any. + let filters = []; + + // Use the query from the dashboard only if no job entities are selected. + let query = undefined; + + const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON'); + if (searchSourceJSON !== undefined) { + const searchSourceData = JSON.parse(searchSourceJSON); + if (searchSourceData.filter !== undefined) { + filters = searchSourceData.filter; + } + query = searchSourceData.query; + } + + // Add time settings to the global state URL parameter with $earliest$ and + // $latest$ tokens which get substituted for times around the time of the + // anomaly on which the URL will be run against. + const _g = rison.encode({ + time: { + from: '$earliest$', + to: '$latest$', + mode: 'absolute' + } + }); + + const appState = { + filters + }; + + // To put entities in filters section would involve creating parameters of the form + // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, + // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) + // which includes the ID of the index holding the field used in the filter. + + // So for simplicity, put entities in the query, replacing any query which is there already. + // e.g. query:(language:kuery,query:'region:us-east-1%20and%20instance:i-20d061fa') + const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); + if (queryFromEntityFieldNames !== undefined) { + query = queryFromEntityFieldNames; + } + + if (query !== undefined) { + appState.query = query; + } + + const _a = rison.encode(appState); + + const urlValue = `kibana#/dashboard/${dashboardId}?_g=${_g}&_a=${_a}`; + + const urlToAdd = { + url_name: settings.label, + url_value: urlValue, + time_range: TIME_RANGE_TYPE.AUTO + }; + + if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { + urlToAdd.time_range = settings.timeRange.interval; + } + + resolve(urlToAdd); + }) + .catch((resp) => { + reject(resp); + }); + + }); + +} + +function buildDiscoverUrlFromSettings(settings) { + const { discoverIndexPatternId, queryFieldNames } = settings.kibanaSettings; + + // Add time settings to the global state URL parameter with $earliest$ and + // $latest$ tokens which get substituted for times around the time of the + // anomaly on which the URL will be run against. + const _g = rison.encode({ + time: { + from: '$earliest$', + to: '$latest$', + mode: 'absolute' + } + }); + + // Add the index pattern and query to the appState part of the URL. + const appState = { + index: discoverIndexPatternId + }; + + // If partitioning field entities have been configured add tokens + // to the URL to use in the Discover page search. + + // Ideally we would put entities in the filters section, but currently this involves creating parameters of the form + // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, + // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) + // which includes the ID of the index holding the field used in the filter. + + // So for simplicity, put entities in the query, replacing any query which is there already. + // e.g. query:(language:kuery,query:'region:us-east-1%20and%20instance:i-20d061fa') + const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); + if (queryFromEntityFieldNames !== undefined) { + appState.query = queryFromEntityFieldNames; + } + + const _a = rison.encode(appState); + + const urlValue = `kibana#/discover?_g=${_g}&_a=${_a}`; + + const urlToAdd = { + url_name: settings.label, + url_value: urlValue, + time_range: TIME_RANGE_TYPE.AUTO, + }; + + if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { + urlToAdd.time_range = settings.timeRange.interval; + } + + return urlToAdd; + +} + +// Builds the query parameter for use in the _a AppState part of a Kibana Dashboard or Discover URL. +function buildAppStateQueryParam(queryFieldNames) { + let queryParam; + if (queryFieldNames !== undefined && queryFieldNames.length > 0) { + let queryString = ''; + queryFieldNames.forEach((fieldName, i) => { + if (i > 0) { + queryString += ' and '; + } + queryString += `${escapeForElasticsearchQuery(fieldName)}:"$${fieldName}$"`; + }); + + queryParam = { + language: 'kuery', + query: queryString + }; + } + + return queryParam; +} + +// Builds the full URL for testing out a custom URL configuration, which +// may contain dollar delimited partition / influencer entity tokens and +// drilldown time range settings. +export function getTestUrl(job, customUrl) { + const urlValue = customUrl.url_value; + const bucketSpanSecs = parseInterval(job.analysis_config.bucket_span).asSeconds(); + + // By default, return configured url_value. Look to substitute any dollar-delimited + // tokens with values from the highest scoring anomaly, or if no anomalies, with + // values from a document returned by the search in the job datafeed. + let testUrl = customUrl.url_value; + + // Query to look for the highest scoring anomaly. + const body = { + query: { + bool: { + must: [ + { term: { job_id: job.job_id } }, + { term: { result_type: 'record' } } + ] + } + }, + size: 1, + _source: { + excludes: [] + }, + sort: [ + { record_score: { order: 'desc' } } + ] + }; + + return new Promise((resolve, reject) => { + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + rest_total_hits_as_int: true, + body + }) + .then((resp) => { + if (resp.hits.total > 0) { + const record = resp.hits.hits[0]._source; + testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, record, 'timestamp'); + resolve(testUrl); + } else { + // No anomalies yet for this job, so do a preview of the search + // configured in the job datafeed to obtain sample docs. + mlJobService.searchPreview(job) + .then((response) => { + let testDoc; + const docTimeFieldName = job.data_description.time_field; + + // Handle datafeeds which use aggregations or documents. + if (response.aggregations) { + // Create a dummy object which contains the fields necessary to build the URL. + const firstBucket = response.aggregations.buckets.buckets[0]; + testDoc = { + [docTimeFieldName]: firstBucket.key + }; + + // Look for bucket aggregations which match the tokens in the URL. + urlValue.replace((/\$([^?&$\'"]{1,40})\$/g), (match, name) => { + if (name !== 'earliest' && name !== 'latest' && firstBucket[name] !== undefined) { + const tokenBuckets = firstBucket[name]; + if (tokenBuckets.buckets) { + testDoc[name] = tokenBuckets.buckets[0].key; + } + } + }); + + } else { + if (response.hits.total > 0) { + testDoc = response.hits.hits[0]._source; + } + } + + if (testDoc !== undefined) { + testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, testDoc, docTimeFieldName); + } + + resolve(testUrl); + + }); + } + + }) + .catch((resp) => { + reject(resp); + }); + }); + +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_jobs_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_jobs_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js index 716ece4b0e2dc..248096ffbd825 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js @@ -27,7 +27,7 @@ import { has } from 'lodash'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ml } from '../../../../services/ml_api_service'; import { SelectSeverity } from '../../../../components/controls/select_severity/select_severity'; import { mlCreateWatchService } from './create_watch_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js index c45894c36b702..447869ff8fdb4 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns'; export const watch = { trigger: { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 9d61d79b1d3e5..a9a81723244f9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { deleteJobs } from '../utils'; -import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../common/constants/jobs_list'; +import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; export const DeleteJobModal = injectI18n(class extends Component { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js index 2b01a84894564..c1c98cddaf368 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js @@ -8,8 +8,8 @@ import { difference } from 'lodash'; import chrome from 'ui/chrome'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { processCreatedBy } from '../../../../../common/util/job_utils'; +import { mlJobService } from '../../../../services/job_service'; +import { processCreatedBy } from '../../../../../../common/util/job_utils'; export function saveJob(job, newJobData, finish) { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js index b09162b0e84cf..a1b4f82e79e66 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js @@ -18,9 +18,9 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import { calculateDatafeedFrequencyDefaultSeconds } from 'plugins/ml/../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../common/util/job_utils'; import { getNewJobDefaults } from '../../../../../services/ml_server_info'; -import { parseInterval } from 'plugins/ml/../common/util/parse_interval'; +import { parseInterval } from '../../../../../../../common/util/parse_interval'; import { MLJobEditor } from '../../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js similarity index 93% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js index 06fe88fcd8714..5c7d040ddbf27 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js @@ -17,8 +17,8 @@ import { EuiSpacer, } from '@elastic/eui'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { detectorToString } from 'plugins/ml/util/string_utils'; +import { mlJobService } from '../../../../../services/job_service'; +import { detectorToString } from '../../../../../util/string_utils'; export class Detectors extends Component { constructor(props) { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index dbdc19d411481..3db5cb970315a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -18,7 +18,7 @@ import { EuiComboBox, } from '@elastic/eui'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../../services/ml_api_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; class JobDetailsUI extends Component { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_job_details.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_job_details.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js similarity index 92% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/datafeed_preview_tab.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index b4bc018519170..a32bb3e1625d2 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -17,9 +17,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { ML_DATA_PREVIEW_COUNT } from 'plugins/ml/../common/util/job_utils'; +import { mlJobService } from '../../../../services/job_service'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 4586109512c1c..028e6a10d6abc 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -7,7 +7,7 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import chrome from 'ui/chrome'; -import { detectorToString } from 'plugins/ml/util/string_utils'; +import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 369b9711ab938..3df869174c146 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -25,11 +25,11 @@ import { import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import chrome from 'ui/chrome'; -import { FORECAST_REQUEST_STATE } from 'plugins/ml/../common/constants/states'; -import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed'; -import { mlForecastService } from 'plugins/ml/services/forecast_service'; +import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; +import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; +import { mlForecastService } from '../../../../../services/forecast_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../../common/util/job_utils'; +import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../../../common/util/job_utils'; const MAX_FORECASTS = 500; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/format_values.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 1d89650c51cae..7513ed355e544 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; -import { toLocaleString } from 'plugins/ml/util/string_utils'; +import { toLocaleString } from '../../../../util/string_utils'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATA_FORMAT = '0.0 b'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details_pane.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index ca80012767c2d..fbb64db94cd56 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -8,7 +8,7 @@ import React, { FC, useEffect, useState } from 'react'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; -import { JobMessage } from '../../../../../common/types/audit_message'; +import { JobMessage } from '../../../../../../common/types/audit_message'; interface JobMessagesPaneProps { jobId: string; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/json_tab.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/json_tab.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/json_tab.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/json_tab.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js index dabfc33e0f6af..60925434c35c7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js @@ -11,7 +11,7 @@ import React, { Fragment, } from 'react'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../services/ml_api_service'; import { JobGroup } from '../job_group'; import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_job_group.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_job_group.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js similarity index 91% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js index b0e10a975b863..d93f9ff7ff454 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js @@ -5,7 +5,7 @@ */ -import { tabColor } from '../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_jobs_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_jobs_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/job_description.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/job_description.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index c160be429817e..fc07d4d2a0294 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -9,7 +9,7 @@ import { timefilter } from 'ui/timefilter'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../services/ml_api_service'; import { checkForAutoStartDatafeed, filterJobs, loadFullJob } from '../utils'; import { JobsList } from '../jobs_list'; import { JobDetails } from '../job_details'; @@ -30,7 +30,7 @@ import { DEFAULT_REFRESH_INTERVAL_MS, DELETING_JOBS_REFRESH_INTERVAL_MS, MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../common/constants/jobs_list'; +} from '../../../../../../common/constants/jobs_list'; let jobsRefreshInterval = null; let deletingJobsRefreshTimeout = null; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js index 83116579a2adb..98f8180f4b980 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js @@ -5,7 +5,7 @@ */ -import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../../common/constants/states'; import { StatsBar } from '../../../../components/stats_bar'; import PropTypes from 'prop-types'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/actions_menu.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index 499d911371d4a..6ba3f531dfb57 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -5,8 +5,8 @@ */ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component, diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index d104d2e02d07b..945f960f4aebe 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -5,7 +5,7 @@ */ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; +import { checkPermission } from '../../../../../privilege/check_privilege'; import PropTypes from 'prop-types'; import React, { Component, diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js similarity index 85% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index 381c9fb0f2671..a94161ee85836 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -6,8 +6,8 @@ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import React from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js new file mode 100644 index 0000000000000..2bb2fd310d175 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -0,0 +1,355 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { each } from 'lodash'; +import { toastNotifications } from 'ui/notify'; +import { mlMessageBarService } from '../../../components/messagebar'; +import rison from 'rison-node'; +import chrome from 'ui/chrome'; + +import { mlJobService } from '../../../services/job_service'; +import { ml } from '../../../services/ml_api_service'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; +import { parseInterval } from '../../../../../common/util/parse_interval'; +import { i18n } from '@kbn/i18n'; + +export function loadFullJob(jobId) { + return new Promise((resolve, reject) => { + ml.jobs.jobs(jobId) + .then((jobs) => { + if (jobs.length) { + resolve(jobs[0]); + } else { + throw new Error(`Could not find job ${jobId}`); + } + }) + .catch((error) => { + reject(error); + }); + }); +} + +export function isStartable(jobs) { + return jobs.some(j => j.datafeedState === DATAFEED_STATE.STOPPED); +} + +export function isStoppable(jobs) { + return jobs.some(j => j.datafeedState === DATAFEED_STATE.STARTED); +} + +export function isClosable(jobs) { + return jobs.some(j => (j.datafeedState === DATAFEED_STATE.STOPPED) && (j.jobState !== JOB_STATE.CLOSED)); +} + +export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { + const datafeedIds = jobs.filter(j => j.hasDatafeed).map(j => j.datafeedId); + mlJobService.forceStartDatafeeds(datafeedIds, start, end) + .then((resp) => { + showResults(resp, DATAFEED_STATE.STARTED); + finish(); + }) + .catch((error) => { + mlMessageBarService.notify.error(error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { + defaultMessage: 'Jobs failed to start' + }), error); + finish(); + }); +} + + +export function stopDatafeeds(jobs, finish = () => {}) { + const datafeedIds = jobs.filter(j => j.hasDatafeed).map(j => j.datafeedId); + mlJobService.stopDatafeeds(datafeedIds) + .then((resp) => { + showResults(resp, DATAFEED_STATE.STOPPED); + finish(); + }) + .catch((error) => { + mlMessageBarService.notify.error(error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { + defaultMessage: 'Jobs failed to stop' + }), error); + finish(); + }); +} + +function showResults(resp, action) { + const successes = []; + const failures = []; + for (const d in resp) { + if (resp[d][action] === true || + (resp[d][action] === false && (resp[d].error.statusCode === 409 && action === DATAFEED_STATE.STARTED))) { + successes.push(d); + } else { + failures.push({ + id: d, + result: resp[d] + }); + } + } + + let actionText = ''; + let actionTextPT = ''; + if (action === DATAFEED_STATE.STARTED) { + actionText = i18n.translate('xpack.ml.jobsList.startActionStatusText', { + defaultMessage: 'start' + }); + actionTextPT = i18n.translate('xpack.ml.jobsList.startedActionStatusText', { + defaultMessage: 'started' + }); + } else if (action === DATAFEED_STATE.STOPPED) { + actionText = i18n.translate('xpack.ml.jobsList.stopActionStatusText', { + defaultMessage: 'stop' + }); + actionTextPT = i18n.translate('xpack.ml.jobsList.stoppedActionStatusText', { + defaultMessage: 'stopped' + }); + } else if (action === DATAFEED_STATE.DELETED) { + actionText = i18n.translate('xpack.ml.jobsList.deleteActionStatusText', { + defaultMessage: 'delete' + }); + actionTextPT = i18n.translate('xpack.ml.jobsList.deletedActionStatusText', { + defaultMessage: 'deleted' + }); + } else if (action === JOB_STATE.CLOSED) { + actionText = i18n.translate('xpack.ml.jobsList.closeActionStatusText', { + defaultMessage: 'close' + }); + actionTextPT = i18n.translate('xpack.ml.jobsList.closedActionStatusText', { + defaultMessage: 'closed' + }); + } + + toastNotifications.addSuccess(i18n.translate('xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage', { + defaultMessage: '{successesJobsCount, plural, one{{successJob}} other{# jobs}} {actionTextPT} successfully', + values: { + successesJobsCount: successes.length, + successJob: successes[0], + actionTextPT + } + })); + + if (failures.length > 0) { + failures.forEach((f) => { + mlMessageBarService.notify.error(f.result.error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.actionFailedNotificationMessage', { + defaultMessage: '{failureId} failed to {actionText}', + values: { + failureId: f.id, + actionText + } + })); + }); + } +} + +export function cloneJob(jobId) { + loadFullJob(jobId) + .then((job) => { + if(job.custom_settings && job.custom_settings.created_by) { + // if the job is from a wizards, i.e. contains a created_by property + // use tempJobCloningObjects to temporarily store the job + mlJobService.tempJobCloningObjects.job = job; + + if ( + job.data_counts.earliest_record_timestamp !== undefined && + job.data_counts.latest_record_timestamp !== undefined && + job.data_counts.latest_bucket_timestamp !== undefined) { + // if the job has run before, use the earliest and latest record timestamp + // as the cloned job's time range + let start = job.data_counts.earliest_record_timestamp; + let end = job.data_counts.latest_record_timestamp; + + if (job.datafeed_config.aggregations !== undefined) { + // if the datafeed uses aggregations the earliest and latest record timestamps may not be the same + // as the start and end of the data in the index. + const bucketSpanMs = parseInterval(job.analysis_config.bucket_span).asMilliseconds(); + // round down to the start of the nearest bucket + start = Math.floor(job.data_counts.earliest_record_timestamp / bucketSpanMs) * bucketSpanMs; + // use latest_bucket_timestamp and add two bucket spans minus one ms + end = job.data_counts.latest_bucket_timestamp + (bucketSpanMs * 2) - 1; + } + + mlJobService.tempJobCloningObjects.start = start; + mlJobService.tempJobCloningObjects.end = end; + } + } else { + // otherwise use the tempJobCloningObjects + mlJobService.tempJobCloningObjects.job = job; + } + window.location.href = '#/jobs/new_job'; + }) + .catch((error) => { + mlMessageBarService.notify.error(error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { + defaultMessage: 'Could not clone {jobId}. Job could not be found', + values: { jobId } + })); + }); +} + +export function closeJobs(jobs, finish = () => {}) { + const jobIds = jobs.map(j => j.id); + mlJobService.closeJobs(jobIds) + .then((resp) => { + showResults(resp, JOB_STATE.CLOSED); + finish(); + }) + .catch((error) => { + mlMessageBarService.notify.error(error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { + defaultMessage: 'Jobs failed to close', + }), error); + finish(); + }); +} + +export function deleteJobs(jobs, finish = () => {}) { + const jobIds = jobs.map(j => j.id); + mlJobService.deleteJobs(jobIds) + .then((resp) => { + showResults(resp, JOB_STATE.DELETED); + finish(); + }) + .catch((error) => { + mlMessageBarService.notify.error(error); + toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { + defaultMessage: 'Jobs failed to delete', + }), error); + finish(); + }); +} + +export function filterJobs(jobs, clauses) { + if (clauses.length === 0) { + return jobs; + } + + // keep count of the number of matches we make as we're looping over the clauses + // we only want to return jobs which match all clauses, i.e. each search term is ANDed + const matches = jobs.reduce((p, c) => { + p[c.id] = { + job: c, + count: 0 + }; + return p; + }, {}); + + clauses.forEach((c) => { + // the search term could be negated with a minus, e.g. -bananas + const bool = (c.match === 'must'); + let js = []; + + if (c.type === 'term') { + // filter term based clauses, e.g. bananas + // match on id, description and memory_status + // if the term has been negated, AND the matches + if (bool === true) { + js = jobs.filter(job => (( + (stringMatch(job.id, c.value) === bool) || + (stringMatch(job.description, c.value) === bool) || + (stringMatch(job.memory_status, c.value) === bool) + ))); + } else { + js = jobs.filter(job => (( + (stringMatch(job.id, c.value) === bool) && + (stringMatch(job.description, c.value) === bool) && + (stringMatch(job.memory_status, c.value) === bool) + ))); + } + } else { + // filter other clauses, i.e. the toggle group buttons + if (Array.isArray(c.value)) { + // the groups value is an array of group ids + js = jobs.filter(job => (jobProperty(job, c.field).some(g => (c.value.indexOf(g) >= 0)))); + } else { + js = jobs.filter(job => (jobProperty(job, c.field) === c.value)); + } + } + + js.forEach(j => (matches[j.id].count++)); + }); + + // loop through the matches and return only those jobs which have match all the clauses + const filteredJobs = []; + each(matches, (m) => { + if (m.count >= clauses.length) { + filteredJobs.push(m.job); + } + }); + return filteredJobs; +} + +// check to see if a job has been stored in mlJobService.tempJobCloningObjects +// if it has, return an object with the minimum properties needed for the +// start datafeed modal. +export function checkForAutoStartDatafeed() { + const job = mlJobService.tempJobCloningObjects.job; + if (job !== undefined) { + mlJobService.tempJobCloningObjects.job = undefined; + const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0); + const datafeedId = hasDatafeed ? job.datafeed_config.datafeed_id : ''; + return { + id: job.job_id, + hasDatafeed, + latestTimestampSortValue: 0, + datafeedId, + }; + } +} + +function stringMatch(str, substr) { + return ( + (typeof str === 'string' && typeof substr === 'string') && + ((str.toLowerCase().match(substr.toLowerCase()) === null) === false) + ); +} + +function jobProperty(job, prop) { + const propMap = { + job_state: 'jobState', + datafeed_state: 'datafeedState', + groups: 'groups', + }; + return job[propMap[prop]]; +} + +export function getJobIdUrl(jobId) { + // Create url for filtering by job id for kibana management table + const settings = { + jobId + }; + const encoded = rison.encode(settings); + const url = `?mlManagement=${encoded}`; + + return `${chrome.getBasePath()}/app/ml#/jobs${url}`; +} + +function getUrlVars(url) { + const vars = {}; + url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) { + vars[key] = value; + }); + return vars; +} + +export function getSelectedJobIdFromUrl(url) { + if (typeof (url) === 'string' && url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const decodedJson = rison.decode(urlParams.mlManagement); + return decodedJson.jobId; + } +} + +export function clearSelectedJobIdFromUrl(url) { + if (typeof (url) === 'string' && url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const clearedParams = `ml#/jobs?_g=${urlParams._g}`; + window.history.replaceState({}, document.title, clearedParams); + } +} + diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js index e55075b0eb850..5fb1b93f7c564 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js @@ -12,7 +12,7 @@ import { validateModelMemoryLimit as validateModelMemoryLimitUtils, validateGroupNames as validateGroupNamesUtils, validateModelMemoryLimitUnits as validateModelMemoryLimitUnitsUtils, -} from '../../../../common/util/job_utils'; +} from '../../../../../common/util/job_utils'; import { isValidLabel, isValidTimeRange } from '../../../util/custom_url_utils'; export function validateModelMemoryLimit(mml) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js new file mode 100644 index 0000000000000..f549ec3826cb5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import ReactDOM from 'react-dom'; +import React from 'react'; + +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { loadIndexPatterns } from '../../util/index_utils'; +import { checkFullLicense } from '../../license/check_license'; +import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; +import { getJobManagementBreadcrumbs } from '../../jobs/breadcrumbs'; +import { loadMlServerInfo } from '../../services/ml_server_info'; + +import uiRoutes from 'ui/routes'; + +const template = ``; + +uiRoutes + .when('/jobs/?', { + template, + k7Breadcrumbs: getJobManagementBreadcrumbs, + resolve: { + CheckLicense: checkFullLicense, + indexPatterns: loadIndexPatterns, + privileges: checkGetJobsPrivilege, + mlNodeCount: getMlNodeCount, + loadMlServerInfo, + } + }); + +import { JobsPage } from './jobs'; +import { I18nContext } from 'ui/i18n'; + +module.directive('jobsPage', function () { + return { + scope: {}, + restrict: 'E', + link: (scope, element) => { + ReactDOM.render( + + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + } + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/chart_loader.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index f80d2e8e0fd55..502a88ecf6004 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -7,8 +7,8 @@ import memoizeOne from 'memoize-one'; import { isEqual } from 'lodash'; import { IndexPattern } from 'ui/index_patterns'; -import { IndexPatternTitle } from '../../../../../common/types/kibana'; -import { Field, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { Field, SplitField, AggFieldPair } from '../../../../../../common/types/fields'; import { ml } from '../../../../services/ml_api_service'; import { mlResultsService } from '../../../../services/results_service'; import { getCategoryFields as getCategoryFieldsOrig } from './searches'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/searches.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/searches.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/job_groups_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx index a71a264662fee..7211c034617f1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/job_groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx @@ -8,7 +8,7 @@ import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { Validation } from '../job_validator'; -import { tabColor } from '../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; import { Description } from '../../pages/components/job_details_step/components/groups/description'; export interface JobGroupsInputProps { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/time_range_picker.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/index_pattern_context.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index_pattern_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/index_pattern_context.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index_pattern_context.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/advanced_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 54e704767b992..22aebc2b88a88 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -8,12 +8,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/type import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField } from '../../../../../common/types/fields'; +import { Field, Aggregation, SplitField } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, CustomRule } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE } from './util/constants'; import { getRichDetectors } from './util/general'; -import { isValidJson } from '../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../common/util/validation_utils'; import { ml } from '../../../../services/ml_api_service'; export interface RichDetector { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/combined_job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/combined_job.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/datafeed.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts similarity index 91% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/datafeed.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts index 68ee45881586b..6c7493c5e52d3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/datafeed.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { JobId } from './job'; export type DatafeedId = string; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/job.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index e5c6964f0f118..86a61e84b445c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -6,17 +6,17 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; -import { IndexPatternTitle } from '../../../../../common/types/kibana'; -import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types'; -import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; +import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { Job, Datafeed, Detector, JobId, DatafeedId, BucketSpan } from './configs'; -import { Aggregation, Field } from '../../../../../common/types/fields'; +import { Aggregation, Field } from '../../../../../../common/types/fields'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; import { JobRunner, ProgressSubscriber } from '../job_runner'; import { JOB_TYPE, CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME } from './util/constants'; import { isSparseDataJob } from './util/general'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; export class JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator_factory.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/multi_metric_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 05a253a0962e9..fea328acb58b3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -7,7 +7,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { + Field, + Aggregation, + SplitField, + AggFieldPair, +} from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL, DEFAULT_MODEL_MEMORY_LIMIT } from './util/constants'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/population_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index ac7161422628d..9e9ccf8ab63e4 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -7,7 +7,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { + Field, + Aggregation, + SplitField, + AggFieldPair, +} from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/single_metric_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index aba3b08f330e8..5f3f6ff310d28 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -6,15 +6,15 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, AggFieldPair } from '../../../../../common/types/fields'; +import { Field, Aggregation, AggFieldPair } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, BucketSpan } from './configs'; import { createBasicDetector } from './util/default_configs'; import { ML_JOB_AGGREGATION, ES_AGGREGATION, -} from '../../../../../common/constants/aggregation_types'; +} from '../../../../../../common/constants/aggregation_types'; import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/type_guards.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/constants.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/constants.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts similarity index 90% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/default_configs.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 2a09415c50bc4..1160401478ab7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -5,8 +5,8 @@ */ import { Job, Datafeed } from '../configs'; -import { IndexPatternTitle } from '../../../../../../common/types/kibana'; -import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; +import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; +import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { Detector } from '../configs'; export function createEmptyJob(): Job { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/general.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 71535ab98c74f..a73c27d954afe 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -10,16 +10,16 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, -} from '../../../../../../common/constants/aggregation_types'; -import { MLCATEGORY } from '../../../../../../common/constants/field_types'; +} from '../../../../../../../common/constants/aggregation_types'; +import { MLCATEGORY } from '../../../../../../../common/constants/field_types'; import { EVENT_RATE_FIELD_ID, Field, AggFieldPair, mlCategory, -} from '../../../../../../common/types/fields'; +} from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../'; +import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from './constants'; const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/job_runner.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts index 4da87dedf14dd..9627d2e477528 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts @@ -9,7 +9,7 @@ import { ml } from '../../../../services/ml_api_service'; import { mlJobService } from '../../../../services/job_service'; import { JobCreator } from '../job_creator'; import { DatafeedId, JobId } from '../job_creator/configs'; -import { DATAFEED_STATE } from '../../../../../common/constants/states'; +import { DATAFEED_STATE } from '../../../../../../common/constants/states'; const REFRESH_INTERVAL_MS = 100; const TARGET_PROGRESS_DELTA = 2; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/job_validator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 82b1684b7b72f..550b579c93392 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -5,7 +5,10 @@ */ import { ReactElement } from 'react'; -import { basicJobValidation, basicDatafeedValidation } from '../../../../../common/util/job_utils'; +import { + basicJobValidation, + basicDatafeedValidation, +} from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { JobCreatorType } from '../job_creator'; import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/util.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/util.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index b1bd352db387b..ab33afb23ef51 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -7,9 +7,12 @@ import { i18n } from '@kbn/i18n'; import { BasicValidations } from './job_validator'; import { Job, Datafeed } from '../job_creator/configs'; -import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { + ALLOWED_DATA_UNITS, + JOB_ID_MAX_LENGTH, +} from '../../../../../../common/constants/validation'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { ValidationResults, ValidationMessage } from '../../../../../common/util/job_utils'; +import { ValidationResults, ValidationMessage } from '../../../../../../common/util/job_utils'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; export function populateValidationMessages( diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts similarity index 92% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/results_loader.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index c9f78c9e8c096..82808ef3d37ee 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -8,13 +8,13 @@ import { BehaviorSubject } from 'rxjs'; import { JobCreatorType, isMultiMetricJobCreator } from '../job_creator'; import { mlResultsService, ModelPlotOutputResults } from '../../../../services/results_service'; import { TimeBuckets } from '../../../../util/time_buckets'; -import { getSeverityType } from '../../../../../common/util/anomaly_utils'; -import { parseInterval } from '../../../../../common/util/parse_interval'; -import { ANOMALY_SEVERITY } from '../../../../../common/constants/anomalies'; +import { getSeverityType } from '../../../../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; +import { ANOMALY_SEVERITY } from '../../../../../../common/constants/anomalies'; import { getScoresByRecord } from './searches'; import { JOB_TYPE } from '../job_creator/util/constants'; import { ChartLoader } from '../chart_loader'; -import { ES_AGGREGATION } from '../../../../../common/constants/aggregation_types'; +import { ES_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; export interface Results { progress: number; @@ -150,15 +150,17 @@ export class ResultsLoader { if (agg === null) { return { [dtrIndex]: [emptyModelItem] }; } - const resp = await mlResultsService.getModelPlotOutput( - this._jobCreator.jobId, - dtrIndex, - [], - this._lastModelTimeStamp, - this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, - agg.mlModelPlotAgg - ); + const resp = await mlResultsService + .getModelPlotOutput( + this._jobCreator.jobId, + dtrIndex, + [], + this._lastModelTimeStamp, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + agg.mlModelPlotAgg + ) + .toPromise(); return this._createModel(resp, dtrIndex); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/searches.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/searches.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index 2e1b022a33b3f..724a6146854af 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from './../../../../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns'; import { escapeForElasticsearchQuery } from '../../../../util/string_utils'; import { ml } from '../../../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx index 1fef1d804e6f2..c5188d045d84f 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx @@ -13,8 +13,8 @@ import React, { Fragment, FC } from 'react'; import { AnnotationDomainTypes, getAnnotationId, LineAnnotation } from '@elastic/charts'; import { Anomaly } from '../../../../common/results_loader'; -import { getSeverityColor } from '../../../../../../../common/util/anomaly_utils'; -import { ANOMALY_THRESHOLD } from '../../../../../../../common/constants/anomalies'; +import { getSeverityColor } from '../../../../../../../../common/util/anomaly_utils'; +import { ANOMALY_THRESHOLD } from '../../../../../../../../common/constants/anomalies'; interface Props { anomalyData?: Anomaly[]; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/axes.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/axes.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/settings.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/utils.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index a284bd20b7ce1..7f5d2bfbe0e90 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -22,7 +22,7 @@ import { CombinedJob } from '../../../../common/job_creator/configs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { JobCreatorContext } from '../../job_creator_context'; import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../common/util/job_utils'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index cf26d6f532d0d..4815629ddd5c8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { Datafeed } from '../../../../common/job_creator/configs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; -import { isValidJson } from '../../../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; const EDITOR_HEIGHT = '800px'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx index 924eacbc4b13c..6328366626894 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; -import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; import { useStringifiedValue } from '../hooks'; export const FrequencyInput: FC = () => { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/hooks.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/hooks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/hooks.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/hooks.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx index dfcedfe7c796e..fa1e7838f5938 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; import { Description } from './description'; -import { isValidJson } from '../../../../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../../../../common/util/validation_utils'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; const EDITOR_HEIGHT = '400px'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx index 3b1993f8e2c7e..f2e2516866835 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/datafeed.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_creator_context.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts index 5fd3c98ed54c9..229fb8c3fd5f2 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_creator_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts @@ -5,7 +5,7 @@ */ import { createContext } from 'react'; -import { Field, Aggregation } from '../../../../../common/types/fields'; +import { Field, Aggregation } from '../../../../../../common/types/fields'; import { TimeBuckets } from '../../../../util/time_buckets'; import { JobCreatorType, SingleMetricJobCreator } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx index d40b756857f46..cf0be9d3c0c4e 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../job_creator_context'; -import { tabColor } from '../../../../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../../../../common/util/group_color_utils'; import { Description } from './description'; export const GroupsInput: FC = () => { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/job_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx similarity index 99% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 5f93361982ea0..06c8068a9c005 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -26,7 +26,7 @@ import { Aggregation, EVENT_RATE_FIELD_ID, mlCategory, -} from '../../../../../../../../common/types/fields'; +} from '../../../../../../../../../common/types/fields'; import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; import { ModalWrapper } from './modal_wrapper'; import { detectorToString } from '../../../../../../../util/string_utils'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx new file mode 100644 index 0000000000000..c4b94c61ac4fb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { Fragment, FC, useContext, useState } from 'react'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { AdvancedJobCreator } from '../../../../../common/job_creator'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { Aggregation, Field } from '../../../../../../../../../common/types/fields'; +import { MetricSelector } from './metric_selector'; +import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; +import { DetectorList } from './detector_list'; +import { ModalPayload } from '../advanced_detector_modal/advanced_detector_modal'; + +interface Props { + setIsValid: (na: boolean) => void; +} + +const emptyRichDetector: RichDetector = { + agg: null, + field: null, + byField: null, + overField: null, + partitionField: null, + excludeFrequent: null, + description: null, + customRules: null, +}; + +export const AdvancedDetectors: FC = ({ setIsValid }) => { + const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext); + const jobCreator = jc as AdvancedJobCreator; + + const { fields, aggs } = newJobCapsService; + const [modalPayload, setModalPayload] = useState(null); + + function closeModal() { + setModalPayload(null); + } + + function detectorChangeHandler(dtr: RichDetector, index?: number) { + if (index === undefined) { + jobCreator.addDetector( + dtr.agg as Aggregation, + dtr.field as Field, + dtr.byField, + dtr.overField, + dtr.partitionField, + dtr.excludeFrequent, + dtr.description + ); + } else { + jobCreator.editDetector( + dtr.agg as Aggregation, + dtr.field as Field, + dtr.byField, + dtr.overField, + dtr.partitionField, + dtr.excludeFrequent, + dtr.description, + index + ); + } + jobCreatorUpdate(); + setModalPayload(null); + } + + function showModal() { + setModalPayload({ detector: emptyRichDetector }); + } + + function onDeleteJob(i: number) { + jobCreator.removeDetector(i); + jobCreatorUpdate(); + } + + function onEditJob(i: number) { + const dtr = jobCreator.richDetectors[i]; + if (dtr !== undefined) { + setModalPayload({ detector: dtr, index: i }); + } + } + + return ( + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx new file mode 100644 index 0000000000000..104b629efd3cb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { Aggregation, Field } from '../../../../../../../../../common/types/fields'; +import { AdvancedDetectorModal, ModalPayload } from '../advanced_detector_modal'; +import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; + +interface Props { + payload: ModalPayload | null; + fields: Field[]; + aggs: Aggregation[]; + detectorChangeHandler: (dtr: RichDetector) => void; + closeModal(): void; + showModal(): void; +} + +const MAX_WIDTH = 560; + +export const MetricSelector: FC = ({ + payload, + fields, + aggs, + detectorChangeHandler, + closeModal, + showModal, +}) => { + return ( + + + + + + + + + + + + {payload !== null && ( + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index bda29b244a5b1..a2434f3c33559 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext, useState, useEffect } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field, Aggregation, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, Aggregation, AggFieldPair } from '../../../../../../../../../common/types/fields'; // The display label used for an aggregation e.g. sum(bytes). export type Label = string; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index f86baaccb84d3..4a1626ffcef89 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -7,7 +7,7 @@ import { useContext, useState } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; -import { EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields'; +import { EVENT_RATE_FIELD_ID } from '../../../../../../../../../common/types/fields'; import { isMultiMetricJobCreator, isPopulationJobCreator, diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx index f9fdba31a0ad4..d995d40284aba 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx index feac3b30dfa3b..a44852b2ad625 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Field, Aggregation, SplitField } from '../../../../../../../../common/types/fields'; +import { Field, Aggregation, SplitField } from '../../../../../../../../../common/types/fields'; interface DetectorTitleProps { index: number; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 293202415ced0..639bdb9ec76bf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx new file mode 100644 index 0000000000000..b76fc120538f5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; + +import { AggFieldPair, SplitField } from '../../../../../../../../../common/types/fields'; +import { ChartSettings } from '../../../charts/common/settings'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { SplitCards, useAnimateSplit } from '../split_cards'; +import { DetectorTitle } from '../detector_title'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + fieldValues: string[]; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; + animate?: boolean; + loading?: boolean; +} + +export const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + fieldValues, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, + loading = false, +}) => { + const animateSplit = useAnimateSplit(); + + return ( + + + {aggFieldPairList.map((af, i) => ( + + + + + + + ))} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx new file mode 100644 index 0000000000000..ffa991388fbe2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { Fragment, FC, useContext, useEffect, useState } from 'react'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { MultiMetricJobCreator } from '../../../../../common/job_creator'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { DropDownLabel, DropDownProps } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { AggFieldPair } from '../../../../../../../../../common/types/fields'; +import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; +import { MetricSelector } from './metric_selector'; +import { ChartGrid } from './chart_grid'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; + +interface Props { + setIsValid: (na: boolean) => void; +} + +export const MultiMetricDetectors: FC = ({ setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + } = useContext(JobCreatorContext); + + const jobCreator = jc as MultiMetricJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState([]); + const [aggFieldPairList, setAggFieldPairList] = useState( + jobCreator.aggFieldPairs + ); + const [lineChartsData, setLineChartsData] = useState({}); + const [loadingData, setLoadingData] = useState(false); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); + const [chartSettings, setChartSettings] = useState(defaultChartSettings); + const [splitField, setSplitField] = useState(jobCreator.splitField); + const [fieldValues, setFieldValues] = useState([]); + const [pageReady, setPageReady] = useState(false); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + addDetector(selectedOptionsIn); + } + + function addDetector(selectedOptionsIn: DropDownLabel[]) { + if (selectedOptionsIn !== null && selectedOptionsIn.length) { + const option = selectedOptionsIn[0] as DropDownLabel; + if (typeof option !== 'undefined') { + const newPair = { agg: option.agg, field: option.field }; + setAggFieldPairList([...aggFieldPairList, newPair]); + setSelectedOptions([]); + } else { + setAggFieldPairList([]); + } + } + } + + function deleteDetector(index: number) { + aggFieldPairList.splice(index, 1); + setAggFieldPairList([...aggFieldPairList]); + } + + useEffect(() => { + setPageReady(true); + }, []); + + // watch for changes in detector list length + useEffect(() => { + jobCreator.removeAllDetectors(); + aggFieldPairList.forEach(pair => { + jobCreator.addDetector(pair.agg, pair.field); + }); + jobCreator.calculateModelMemoryLimit(); + jobCreatorUpdate(); + loadCharts(); + setIsValid(aggFieldPairList.length > 0); + }, [aggFieldPairList.length]); + + // watch for change in jobCreator + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadCharts(); + } + + if (jobCreator.bucketSpanMs !== bucketSpanMs) { + setBucketSpanMs(jobCreator.bucketSpanMs); + loadCharts(); + } + + setSplitField(jobCreator.splitField); + }, [jobCreatorUpdated]); + + // watch for changes in split field. + // load example field values + // changes to fieldValues here will trigger the card effect + useEffect(() => { + if (splitField !== null) { + chartLoader + .loadFieldExampleValues(splitField) + .then(setFieldValues) + .catch(error => { + mlMessageBarService.notify.error(error); + }); + } else { + setFieldValues([]); + } + jobCreator.calculateModelMemoryLimit(); + }, [splitField]); + + // watch for changes in the split field values + // reload the charts + useEffect(() => { + loadCharts(); + }, [fieldValues]); + + async function loadCharts() { + if (allDataReady()) { + setLoadingData(true); + try { + const cs = getChartSettings(jobCreator, chartInterval); + setChartSettings(cs); + const resp: LineChartData = await chartLoader.loadLineCharts( + jobCreator.start, + jobCreator.end, + aggFieldPairList, + jobCreator.splitField, + fieldValues.length > 0 ? fieldValues[0] : null, + cs.intervalMs + ); + setLineChartsData(resp); + } catch (error) { + mlMessageBarService.notify.error(error); + setLineChartsData([]); + } + setLoadingData(false); + } + } + + function allDataReady() { + return ( + pageReady && + aggFieldPairList.length > 0 && + (splitField === null || (splitField !== null && fieldValues.length > 0)) + ); + } + + return ( + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx new file mode 100644 index 0000000000000..dd35dc136e70d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; +import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; + +interface Props { + fields: Field[]; + detectorChangeHandler: (options: DropDownLabel[]) => void; + selectedOptions: DropDownProps; + maxWidth?: number; + removeOptions: AggFieldPair[]; +} + +const MAX_WIDTH = 560; + +export const MetricSelector: FC = ({ + fields, + detectorChangeHandler, + selectedOptions, + maxWidth, + removeOptions, +}) => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx new file mode 100644 index 0000000000000..8cd533f8b2e29 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { AggFieldPair, SplitField } from '../../../../../../../../../common/types/fields'; +import { ChartSettings } from '../../../charts/common/settings'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { SplitCards, useAnimateSplit } from '../split_cards'; +import { DetectorTitle } from '../detector_title'; +import { ByFieldSelector } from '../split_field'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; + +type DetectorFieldValues = Record; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; + fieldValuesPerDetector: DetectorFieldValues; + loading?: boolean; +} + +export const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, + fieldValuesPerDetector, + loading = false, +}) => { + const animateSplit = useAnimateSplit(); + + return ( + + {aggFieldPairList.map((af, i) => ( + + + + + + {deleteDetector !== undefined && } + + {jobType === JOB_TYPE.POPULATION && } + + + + + + + + ))} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx new file mode 100644 index 0000000000000..fe5d3ff0c29fb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { Fragment, FC, useContext, useEffect, useState, useReducer } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { PopulationJobCreator } from '../../../../../common/job_creator'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { DropDownLabel, DropDownProps } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; +import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; +import { MetricSelector } from './metric_selector'; +import { SplitFieldSelector } from '../split_field'; +import { ChartGrid } from './chart_grid'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; + +interface Props { + setIsValid: (na: boolean) => void; +} + +type DetectorFieldValues = Record; + +export const PopulationDetectors: FC = ({ setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + } = useContext(JobCreatorContext); + const jobCreator = jc as PopulationJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState([]); + const [aggFieldPairList, setAggFieldPairList] = useState( + jobCreator.aggFieldPairs + ); + const [lineChartsData, setLineChartsData] = useState({}); + const [loadingData, setLoadingData] = useState(false); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); + const [chartSettings, setChartSettings] = useState(defaultChartSettings); + const [splitField, setSplitField] = useState(jobCreator.splitField); + const [fieldValuesPerDetector, setFieldValuesPerDetector] = useState({}); + const [byFieldsUpdated, setByFieldsUpdated] = useReducer<(s: number) => number>(s => s + 1, 0); + const [pageReady, setPageReady] = useState(false); + const updateByFields = () => setByFieldsUpdated(0); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + addDetector(selectedOptionsIn); + } + + function addDetector(selectedOptionsIn: DropDownLabel[]) { + if (selectedOptionsIn !== null && selectedOptionsIn.length) { + const option = selectedOptionsIn[0] as DropDownLabel; + if (typeof option !== 'undefined') { + const newPair = { agg: option.agg, field: option.field, by: { field: null, value: null } }; + setAggFieldPairList([...aggFieldPairList, newPair]); + setSelectedOptions([]); + } else { + setAggFieldPairList([]); + } + } + } + + function deleteDetector(index: number) { + aggFieldPairList.splice(index, 1); + setAggFieldPairList([...aggFieldPairList]); + updateByFields(); + } + + useEffect(() => { + setPageReady(true); + }, []); + + // watch for changes in detector list length + useEffect(() => { + jobCreator.removeAllDetectors(); + aggFieldPairList.forEach((pair, i) => { + jobCreator.addDetector(pair.agg, pair.field); + if (pair.by !== undefined) { + // re-add by fields + jobCreator.setByField(pair.by.field, i); + } + }); + jobCreatorUpdate(); + loadCharts(); + setIsValid(aggFieldPairList.length > 0); + }, [aggFieldPairList.length]); + + // watch for changes in by field values + // redraw the charts if they change. + // triggered when example fields have been loaded + // if the split field or by fields have changed + useEffect(() => { + loadCharts(); + }, [JSON.stringify(fieldValuesPerDetector), splitField, pageReady]); + + // watch for change in jobCreator + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadCharts(); + } + + if (jobCreator.bucketSpanMs !== bucketSpanMs) { + setBucketSpanMs(jobCreator.bucketSpanMs); + loadCharts(); + } + + setSplitField(jobCreator.splitField); + + // update by fields and their by fields + let update = false; + const newList = [...aggFieldPairList]; + newList.forEach((pair, i) => { + const bf = jobCreator.getByField(i); + if (pair.by !== undefined && pair.by.field !== bf) { + pair.by.field = bf; + update = true; + } + }); + if (update) { + setAggFieldPairList(newList); + updateByFields(); + } + }, [jobCreatorUpdated]); + + // watch for changes in split field or by fields. + // load example field values + // changes to fieldValues here will trigger the card effect via setFieldValuesPerDetector + useEffect(() => { + loadFieldExamples(); + }, [splitField, byFieldsUpdated]); + + async function loadCharts() { + if (allDataReady()) { + setLoadingData(true); + try { + const cs = getChartSettings(jobCreator, chartInterval); + setChartSettings(cs); + const resp: LineChartData = await chartLoader.loadPopulationCharts( + jobCreator.start, + jobCreator.end, + aggFieldPairList, + jobCreator.splitField, + cs.intervalMs + ); + + setLineChartsData(resp); + } catch (error) { + mlMessageBarService.notify.error(error); + setLineChartsData([]); + } + setLoadingData(false); + } + } + + async function loadFieldExamples() { + const promises: any[] = []; + aggFieldPairList.forEach((af, i) => { + if (af.by !== undefined && af.by.field !== null) { + promises.push( + (async (index: number, field: Field) => { + return { + index, + fields: await chartLoader.loadFieldExampleValues(field), + }; + })(i, af.by.field) + ); + } + }); + const results = await Promise.all(promises); + const fieldValues = results.reduce((p, c) => { + p[c.index] = c.fields; + return p; + }, {}) as DetectorFieldValues; + + const newPairs = aggFieldPairList.map((pair, i) => ({ + ...pair, + ...(pair.by === undefined || pair.by.field === null + ? {} + : { + by: { + ...pair.by, + value: fieldValues[i][0], + }, + }), + })); + setAggFieldPairList([...newPairs]); + setFieldValuesPerDetector(fieldValues); + } + + function allDataReady() { + let ready = aggFieldPairList.length > 0; + aggFieldPairList.forEach(af => { + if (af.by !== undefined && af.by.field !== null) { + // if a by field is set, it's only ready when the value is loaded + ready = ready && af.by.value !== null; + } + }); + return ready; + } + + return ( + + + {splitField !== null && } + + {splitField !== null && ( + + )} + {splitField !== null && ( + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index b13f8e3a73a10..4474a2d6b5413 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -12,7 +12,7 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { PopulationJobCreator } from '../../../../../common/job_creator'; import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import { LineChartData } from '../../../../../common/chart_loader'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; import { mlMessageBarService } from '../../../../../../../components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx new file mode 100644 index 0000000000000..9857a585c14b8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; +import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; + +interface Props { + fields: Field[]; + detectorChangeHandler: (options: DropDownLabel[]) => void; + selectedOptions: DropDownProps; + maxWidth?: number; + removeOptions: AggFieldPair[]; +} + +const MAX_WIDTH = 560; + +export const MetricSelector: FC = ({ + fields, + detectorChangeHandler, + selectedOptions, + maxWidth, + removeOptions, +}) => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx new file mode 100644 index 0000000000000..f04b63f47789e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { SingleMetricJobCreator } from '../../../../../common/job_creator'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { AggSelect, DropDownLabel, DropDownProps, createLabel } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { AggFieldPair } from '../../../../../../../../../common/types/fields'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { getChartSettings } from '../../../charts/common/settings'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; + +interface Props { + setIsValid: (na: boolean) => void; +} + +const DTR_IDX = 0; + +export const SingleMetricDetectors: FC = ({ setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + } = useContext(JobCreatorContext); + const jobCreator = jc as SingleMetricJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState( + jobCreator.aggFieldPair !== null ? [{ label: createLabel(jobCreator.aggFieldPair) }] : [] + ); + const [aggFieldPair, setAggFieldPair] = useState(jobCreator.aggFieldPair); + const [lineChartsData, setLineChartData] = useState({}); + const [loadingData, setLoadingData] = useState(false); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + setSelectedOptions(selectedOptionsIn); + if (selectedOptionsIn.length) { + const option = selectedOptionsIn[0]; + if (typeof option !== 'undefined') { + setAggFieldPair({ agg: option.agg, field: option.field }); + } else { + setAggFieldPair(null); + } + } + } + + useEffect(() => { + if (aggFieldPair !== null) { + jobCreator.setDetector(aggFieldPair.agg, aggFieldPair.field); + jobCreatorUpdate(); + loadChart(); + setIsValid(aggFieldPair !== null); + } + }, [aggFieldPair]); + + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadChart(); + } + + if (jobCreator.bucketSpanMs !== bucketSpanMs) { + setBucketSpanMs(jobCreator.bucketSpanMs); + loadChart(); + } + }, [jobCreatorUpdated]); + + async function loadChart() { + if (aggFieldPair !== null) { + setLoadingData(true); + try { + const cs = getChartSettings(jobCreator, chartInterval); + const resp: LineChartData = await chartLoader.loadLineCharts( + jobCreator.start, + jobCreator.end, + [aggFieldPair], + null, + null, + cs.intervalMs + ); + if (resp[DTR_IDX] !== undefined) { + setLineChartData(resp); + } + } catch (error) { + mlMessageBarService.notify.error(error); + setLineChartData({}); + } + setLoadingData(false); + } + } + + return ( + + + {(lineChartsData[DTR_IDX] !== undefined || loadingData === true) && ( + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx index 76461e1306333..2884bce4d89ad 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiSwitch } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; -import { ES_AGGREGATION } from '../../../../../../../../common/constants/aggregation_types'; +import { ES_AGGREGATION } from '../../../../../../../../../common/constants/aggregation_types'; export const SparseDataSwitch: FC = () => { const { jobCreator, jobCreatorUpdated, jobCreatorUpdate } = useContext(JobCreatorContext); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index b387d4a2fc34f..918163572076c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -8,7 +8,7 @@ import React, { FC, memo, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { SplitField } from '../../../../../../../../common/types/fields'; +import { SplitField } from '../../../../../../../../../common/types/fields'; import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx index d08e989c49dea..fc78e8e244193 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; import { MultiMetricJobCreator, PopulationJobCreator } from '../../../../../common/job_creator'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx index 0dc76be9f8f07..378c088332ed4 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; -import { Field, SplitField } from '../../../../../../../../common/types/fields'; +import { Field, SplitField } from '../../../../../../../../../common/types/fields'; interface DropDownLabel { label: string; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index 2f240344e0ea5..efe32e3173cad 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/step_types.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/step_types.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/step_types.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/step_types.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/common.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/common.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/common.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/common.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx index 5e1bf9f1ec889..c624972aa07ea 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiDescriptionList, EuiFormRow } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; -import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, defaultLabel, Italic } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index a6ef18d4931b9..de019cbe86f9d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -12,9 +12,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; // @ts-ignore -import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout'; -import { JobCreatorContext } from '../../../../components/job_creator_context'; -import { DATAFEED_STATE } from '../../../../../../../../common/constants/states'; +import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { DATAFEED_STATE } from '../../../../../../../../../common/constants/states'; interface Props { jobRunner: JobRunner | null; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/time_range.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/validation.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js new file mode 100644 index 0000000000000..ffa16930e79f2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from '../../../../../util/index_utils'; + +describe('ML - Index or Saved Search selection directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Index or Saved Search selection directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx new file mode 100644 index 0000000000000..9bd653708d9c0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../../../common/types/angular'; +import { Page } from './page'; + +module.directive('mlIndexOrSearch', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const $route = $injector.get('$route'); + const { nextStepPath } = $route.current.locals; + + ReactDOM.render( + {React.createElement(Page, { nextStepPath })}, + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js new file mode 100644 index 0000000000000..bdf65e3bafe96 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from '../../../../../util/index_utils'; + +describe('ML - Job Type Directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Job Type Directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx new file mode 100644 index 0000000000000..8d54ca65a2852 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../../../common/types/angular'; +import { createSearchItems } from '../../utils/new_job_utils'; +import { Page } from './page'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; + +module.directive('mlJobTypePage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page)} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx new file mode 100644 index 0000000000000..db4078ba1bbc8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../../../common/types/angular'; +import { createSearchItems } from '../../utils/new_job_utils'; +import { Page, PageProps } from './page'; +import { JOB_TYPE } from '../../common/job_creator/util/constants'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; + +module.directive('mlNewJobPage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; + + if ($route.current.locals.jobType === undefined) { + return; + } + const jobType: JOB_TYPE = $route.current.locals.jobType; + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + const props: PageProps = { + existingJobsAndGroups, + jobType, + }; + + ReactDOM.render( + + + {React.createElement(Page, props)} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts similarity index 96% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts index 7f3f6c364e048..a527d92342d4c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/route.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts @@ -17,7 +17,7 @@ import { getAdvancedJobConfigurationBreadcrumbs, } from '../../../breadcrumbs'; -import { Route } from '../../../../../common/types/kibana'; +import { Route } from '../../../../../../common/types/kibana'; import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_steps.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js new file mode 100644 index 0000000000000..d5d5ee4438e32 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from '../../../../util/index_utils'; + +describe('ML - Recognize job directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Recognize job directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/create_result_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/create_result_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/edit_job.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx index 7ec8cddfe3ed5..0dd222a1726ef 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/edit_job.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx @@ -22,11 +22,11 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { usePartialState } from '../../../../components/custom_hooks'; -import { composeValidators, maxLengthValidator } from '../../../../../common/util/validators'; -import { isJobIdValid } from '../../../../../common/util/job_utils'; -import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { composeValidators, maxLengthValidator } from '../../../../../../common/util/validators'; +import { isJobIdValid } from '../../../../../../common/util/job_utils'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; import { JobGroupsInput } from '../../common/components'; -import { JobOverride } from '../../../../../common/types/modules'; +import { JobOverride } from '../../../../../../common/types/modules'; interface EditJobProps { job: ModuleJobUI; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_item.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx similarity index 97% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_item.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index ace8409734b74..2a15a42ba04f8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; -import { tabColor } from '../../../../../common/util/group_color_utils'; -import { JobOverride } from '../../../../../common/types/modules'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; +import { JobOverride } from '../../../../../../common/types/modules'; interface JobItemProps { job: ModuleJobUI; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_settings_form.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index bae16d620af5b..4046bd8b09afa 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -25,8 +25,8 @@ import { composeValidators, maxLengthValidator, patternValidator, -} from '../../../../../common/util/validators'; -import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +} from '../../../../../../common/util/validators'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; import { usePartialState } from '../../../../components/custom_hooks'; import { TimeRange, TimeRangePicker } from '../../common/components'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/kibana_objects.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/module_jobs.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/module_jobs.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx index adae037305ff8..7c72dc63691fa 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/module_jobs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx @@ -17,7 +17,7 @@ import { import { JobOverrides, ModuleJobUI, SAVE_STATE } from '../page'; import { JobItem } from './job_item'; import { EditJob } from './edit_job'; -import { JobOverride } from '../../../../../common/types/modules'; +import { JobOverride } from '../../../../../../common/types/modules'; interface ModuleJobsProps { jobs: ModuleJobUI[]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx new file mode 100644 index 0000000000000..2d08a1da07459 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../../common/types/angular'; + +import { createSearchItems } from '../utils/new_job_utils'; +import { Page } from './page'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; + +module.directive('mlRecognizePage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kibanaConfig = $injector.get('config'); + const $route = $injector.get('$route'); + + const moduleId = $route.current.params.id; + const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; + + const { indexPattern, savedSearch, combinedQuery } = createSearchItems( + kibanaConfig, + $route.current.locals.indexPattern, + $route.current.locals.savedSearch + ); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page, { moduleId, existingGroupIds })} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx new file mode 100644 index 0000000000000..11b2a8f01342d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -0,0 +1,335 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, useState, Fragment, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiTitle, + EuiPageHeaderSection, + EuiPageHeader, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiSpacer, + EuiCallOut, + EuiPanel, +} from '@elastic/eui'; +import { toastNotifications } from 'ui/notify'; +import { merge } from 'lodash'; +import { ml } from '../../../services/ml_api_service'; +import { useKibanaContext } from '../../../contexts/kibana'; +import { + DatafeedResponse, + DataRecognizerConfigResponse, + JobOverride, + JobResponse, + KibanaObject, + KibanaObjectResponse, + Module, + ModuleJob, +} from '../../../../../common/types/modules'; +import { mlJobService } from '../../../services/job_service'; +import { CreateResultCallout } from './components/create_result_callout'; +import { KibanaObjects } from './components/kibana_objects'; +import { ModuleJobs } from './components/module_jobs'; +import { checkForSavedObjects } from './resolvers'; +import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form'; +import { TimeRange } from '../common/components'; +import { JobId } from '../common/job_creator/configs'; + +export interface ModuleJobUI extends ModuleJob { + datafeedResult?: DatafeedResponse; + setupResult?: JobResponse; +} + +export type KibanaObjectUi = KibanaObject & KibanaObjectResponse; + +export interface KibanaObjects { + [objectType: string]: KibanaObjectUi[]; +} + +interface PageProps { + moduleId: string; + existingGroupIds: string[]; +} + +export type JobOverrides = Record; + +export enum SAVE_STATE { + NOT_SAVED, + SAVING, + SAVED, + FAILED, + PARTIAL_FAILURE, +} + +export const Page: FC = ({ moduleId, existingGroupIds }) => { + // #region State + const [jobPrefix, setJobPrefix] = useState(''); + const [jobs, setJobs] = useState([]); + const [jobOverrides, setJobOverrides] = useState({}); + const [kibanaObjects, setKibanaObjects] = useState({}); + const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); + const [resultsUrl, setResultsUrl] = useState(''); + // #endregion + + const { + currentSavedSearch: savedSearch, + currentIndexPattern: indexPattern, + combinedQuery, + } = useKibanaContext(); + const pageTitle = + savedSearch.id !== undefined + ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: savedSearch.title }, + }) + : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: indexPattern.title }, + }); + const displayQueryWarning = savedSearch.id !== undefined; + const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; + + /** + * Loads recognizer module configuration. + */ + const loadModule = async () => { + try { + const response: Module = await ml.getDataRecognizerModule({ moduleId }); + setJobs(response.jobs); + + const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects); + setKibanaObjects(kibanaObjectsResult); + + setSaveState(SAVE_STATE.NOT_SAVED); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }; + + const getTimeRange = async ( + useFullIndexData: boolean, + timeRange: TimeRange + ): Promise => { + if (useFullIndexData) { + const { start, end } = await ml.getTimeFieldRange({ + index: indexPattern.title, + timeFieldName: indexPattern.timeFieldName, + query: combinedQuery, + }); + return { + start: start.epoch, + end: end.epoch, + }; + } else { + return Promise.resolve(timeRange); + } + }; + + useEffect(() => { + loadModule(); + }, []); + + /** + * Sets up recognizer module configuration. + */ + const save = async (formValues: JobSettingsFormValues) => { + setSaveState(SAVE_STATE.SAVING); + const { + jobPrefix: resultJobPrefix, + startDatafeedAfterSave, + useDedicatedIndex, + useFullIndexData, + timeRange, + } = formValues; + + const resultTimeRange = await getTimeRange(useFullIndexData, timeRange); + + try { + let jobOverridesPayload: JobOverride[] | null = Object.values(jobOverrides); + jobOverridesPayload = jobOverridesPayload.length > 0 ? jobOverridesPayload : null; + + const response: DataRecognizerConfigResponse = await ml.setupDataRecognizerConfig({ + moduleId, + prefix: resultJobPrefix, + query: tempQuery, + indexPatternName: indexPattern.title, + useDedicatedIndex, + startDatafeed: startDatafeedAfterSave, + ...(jobOverridesPayload !== null ? { jobOverrides: jobOverridesPayload } : {}), + ...resultTimeRange, + }); + const { datafeeds: datafeedsResponse, jobs: jobsResponse, kibana: kibanaResponse } = response; + + setJobs( + jobs.map(job => { + return { + ...job, + datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)), + setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id), + }; + }) + ); + setKibanaObjects(merge(kibanaObjects, kibanaResponse)); + setResultsUrl( + mlJobService.createResultsUrl( + jobsResponse.filter(({ success }) => success).map(({ id }) => id), + resultTimeRange.start, + resultTimeRange.end, + 'explorer' + ) + ); + const failedJobsCount = jobsResponse.reduce((count, { success }) => { + return success ? count : count + 1; + }, 0); + setSaveState( + failedJobsCount === 0 + ? SAVE_STATE.SAVED + : failedJobsCount === jobs.length + ? SAVE_STATE.FAILED + : SAVE_STATE.PARTIAL_FAILURE + ); + } catch (e) { + setSaveState(SAVE_STATE.FAILED); + // eslint-disable-next-line no-console + console.error('Error setting up module', e); + toastNotifications.addDanger({ + title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { + defaultMessage: 'Error setting up module {moduleId}', + values: { moduleId }, + }), + text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', { + defaultMessage: + 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', + values: { + count: jobs.length, + }, + }), + }); + } + }; + + const onJobOverridesChange = (job: JobOverride) => { + setJobOverrides({ + ...jobOverrides, + [job.job_id as string]: job, + }); + }; + + const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); + + return ( + + + + + +

+ +

+
+
+
+ + {displayQueryWarning && ( + <> + + } + color="warning" + iconType="alert" + > + + + + + + + )} + + + + + +

+ +

+
+ + + + {isFormVisible && ( + { + setJobPrefix(formValues.jobPrefix); + }} + saveState={saveState} + jobs={jobs} + /> + )} + +
+
+ + + + + {Object.keys(kibanaObjects).length > 0 && ( + <> + + + {Object.keys(kibanaObjects).map((objectType, i) => ( + + + {i < Object.keys(kibanaObjects).length - 1 && } + + ))} + + + )} + +
+ +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/resolvers.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts similarity index 90% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index cb4e7a21997e6..455fac9b532d6 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -7,7 +7,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; export interface SearchItems { indexPattern: IIndexPattern; @@ -28,7 +28,7 @@ export function createSearchItems( // a lucene query_string. // Using a blank query will cause match_all:{} to be used // when passed through luceneStringToDsl - let query = { + let query: Query = { query: '', language: 'lucene', }; @@ -45,12 +45,12 @@ export function createSearchItems( if (indexPattern.id === undefined && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index')!; - query = searchSource.getField('query'); + query = searchSource.getField('query')!; const fs = searchSource.getField('filter'); - const filters = fs.length ? fs : []; + const filters = Array.isArray(fs) ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); diff --git a/x-pack/legacy/plugins/ml/public/license/__tests__/check_license.js b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js similarity index 84% rename from x-pack/legacy/plugins/ml/public/license/__tests__/check_license.js rename to x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js index e620a29e1d7f1..7d167fa066fda 100644 --- a/x-pack/legacy/plugins/ml/public/license/__tests__/check_license.js +++ b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; +import { xpackInfo } from '../../../../../xpack_main/public/services/xpack_info'; +import { LICENSE_STATUS_VALID } from '../../../../../../common/constants/license_status'; import { xpackFeatureAvailable, } from '../check_license'; diff --git a/x-pack/legacy/plugins/ml/public/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx similarity index 94% rename from x-pack/legacy/plugins/ml/public/license/check_license.tsx rename to x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index 8457e462567cc..c184a4d4e94e0 100644 --- a/x-pack/legacy/plugins/ml/public/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { banners } from 'ui/notify'; import { EuiCallOut } from '@elastic/eui'; // @ts-ignore No declaration file for module -import { xpackInfo } from '../../../xpack_main/public/services/xpack_info'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import { LICENSE_STATUS_VALID } from '../../../../common/constants/license_status'; +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; +import { LICENSE_TYPE } from '../../../common/constants/license'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; let licenseHasExpired = true; let licenseType: LICENSE_TYPE | null = null; diff --git a/x-pack/legacy/plugins/ml/public/application/management/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/_index.scss new file mode 100644 index 0000000000000..e14df2d7c2039 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/management/_index.scss @@ -0,0 +1 @@ +@import 'jobs_list/index'; diff --git a/x-pack/legacy/plugins/ml/public/management/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts new file mode 100644 index 0000000000000..092639cd5fbab --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { management } from 'ui/management'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore No declaration file for module +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; +import { JOBS_LIST_PATH } from './management_urls'; +import { LICENSE_TYPE } from '../../../common/constants/license'; +import './jobs_list'; + +if ( + xpackInfo.get('features.ml.showLinks', false) === true && + xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL +) { + management.register('ml', { + display: i18n.translate('xpack.ml.management.mlTitle', { + defaultMessage: 'Machine Learning', + }), + order: 100, + icon: 'machineLearningApp', + }); + + management.getSection('ml').register('jobsList', { + name: 'jobsListLink', + order: 10, + display: i18n.translate('xpack.ml.management.jobsListTitle', { + defaultMessage: 'Jobs list', + }), + url: `#${JOBS_LIST_PATH}`, + }); +} diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss new file mode 100644 index 0000000000000..841415620d691 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss @@ -0,0 +1 @@ +@import 'components/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss new file mode 100644 index 0000000000000..b9e7d17ca209f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss @@ -0,0 +1,4 @@ +@import 'jobs_list_page/stats_bar'; +@import 'jobs_list_page/buttons'; +@import 'jobs_list_page/expanded_row'; +@import 'jobs_list_page/analytics_table'; diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/access_denied_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/access_denied_page.tsx rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_analytics_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_analytics_table.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_buttons.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_buttons.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_expanded_row.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_expanded_row.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_stats_bar.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_stats_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_stats_bar.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_stats_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx similarity index 99% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index e3188c0892580..a19a27d00e9b0 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -21,7 +21,7 @@ import { import { metadata } from 'ui/metadata'; // @ts-ignore undeclared module -import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view'; +import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/management_urls.ts b/x-pack/legacy/plugins/ml/public/application/management/management_urls.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/management_urls.ts rename to x-pack/legacy/plugins/ml/public/application/management/management_urls.ts diff --git a/x-pack/legacy/plugins/ml/public/ml.svg b/x-pack/legacy/plugins/ml/public/application/ml.svg similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml.svg rename to x-pack/legacy/plugins/ml/public/application/ml.svg diff --git a/x-pack/legacy/plugins/ml/public/ml_nodes_check/check_ml_nodes.ts b/x-pack/legacy/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml_nodes_check/check_ml_nodes.ts rename to x-pack/legacy/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts diff --git a/x-pack/legacy/plugins/ml/public/ml_nodes_check/index.ts b/x-pack/legacy/plugins/ml/public/application/ml_nodes_check/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml_nodes_check/index.ts rename to x-pack/legacy/plugins/ml/public/application/ml_nodes_check/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/overview/_index.scss b/x-pack/legacy/plugins/ml/public/application/overview/_index.scss new file mode 100644 index 0000000000000..841415620d691 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/_index.scss @@ -0,0 +1 @@ +@import 'components/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts new file mode 100644 index 0000000000000..9df503b462b6c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +export function getOverviewBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.overviewBreadcrumbs.overviewLabel', { + defaultMessage: 'Overview', + }), + href: '', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/overview/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/overview/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/_index.scss rename to x-pack/legacy/plugins/ml/public/application/overview/components/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx new file mode 100644 index 0000000000000..156e53b19874f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState } from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + mlInMemoryTableFactory, + SortDirection, + SORT_DIRECTION, + OnTableChangeArg, + ColumnType, +} from '../../../components/ml_in_memory_table'; +import { getAnalysisType } from '../../../data_frame_analytics/common/analytics'; +import { + DataFrameAnalyticsListColumn, + DataFrameAnalyticsListRow, +} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { + getTaskStateBadge, + progressColumn, +} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns'; +import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions'; +import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; + +interface Props { + items: DataFrameAnalyticsListRow[]; +} +export const AnalyticsTable: FC = ({ items }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const [sortField, setSortField] = useState(DataFrameAnalyticsListColumn.id); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + // id, type, status, progress, created time, view icon + const columns: Array> = [ + { + field: DataFrameAnalyticsListColumn.id, + name: i18n.translate('xpack.ml.overview.analyticsList.id', { defaultMessage: 'ID' }), + sortable: true, + truncateText: true, + width: '20%', + }, + { + name: i18n.translate('xpack.ml.overview.analyticsList.type', { defaultMessage: 'Type' }), + sortable: (item: DataFrameAnalyticsListRow) => getAnalysisType(item.config.analysis), + truncateText: true, + render(item: DataFrameAnalyticsListRow) { + return {getAnalysisType(item.config.analysis)}; + }, + width: '150px', + }, + { + name: i18n.translate('xpack.ml.overview.analyticsList.status', { defaultMessage: 'Status' }), + sortable: (item: DataFrameAnalyticsListRow) => item.stats.state, + truncateText: true, + render(item: DataFrameAnalyticsListRow) { + return getTaskStateBadge(item.stats.state, item.stats.reason); + }, + width: '100px', + }, + progressColumn, + { + field: DataFrameAnalyticsListColumn.configCreateTime, + name: i18n.translate('xpack.ml.overview.analyticsList.reatedTimeColumnName', { + defaultMessage: 'Creation time', + }), + dataType: 'date', + render: (time: number) => formatHumanReadableDateTimeSeconds(time), + textOnly: true, + truncateText: true, + sortable: true, + width: '20%', + }, + { + name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', { + defaultMessage: 'Actions', + }), + actions: [AnalyticsViewAction], + width: '100px', + }, + ]; + + const onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: DataFrameAnalyticsListColumn.id, direction: SORT_DIRECTION.ASC }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + }; + + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: items.length, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }; + + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + const MlInMemoryTable = mlInMemoryTableFactory(); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx new file mode 100644 index 0000000000000..f638094cfb434 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC } from 'react'; +import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore no module file +import { getLink } from '../../../jobs/jobs_list/components/job_actions/results'; +import { MlSummaryJobs } from '../../../../../common/types/jobs'; + +interface Props { + jobsList: MlSummaryJobs; +} + +export const ExplorerLink: FC = ({ jobsList }) => { + const openJobsInAnomalyExplorerText = i18n.translate( + 'xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText', + { + defaultMessage: 'Open {jobsCount, plural, one {{jobId}} other {# jobs}} in Anomaly Explorer', + values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id }, + } + ); + + return ( + + + {i18n.translate('xpack.ml.overview.anomalyDetection.exploreActionName', { + defaultMessage: 'Explore', + })} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx similarity index 98% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 3c89e72ee4943..1f9d0413d45f9 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -20,8 +20,8 @@ import { toastNotifications } from 'ui/notify'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; -import { Dictionary } from '../../../../common/types/common'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs'; +import { Dictionary } from '../../../../../common/types/common'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; export type GroupsDictionary = Dictionary; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx new file mode 100644 index 0000000000000..4a2d8ea24e7f5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, Fragment, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + mlInMemoryTableFactory, + SortDirection, + SORT_DIRECTION, + OnTableChangeArg, + ColumnType, +} from '../../../components/ml_in_memory_table'; +import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; +import { ExplorerLink } from './actions'; +import { getJobsFromGroup } from './utils'; +import { GroupsDictionary, Group } from './anomaly_detection_panel'; +import { MlSummaryJobs } from '../../../../../common/types/jobs'; +import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar'; +// @ts-ignore +import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge/index'; +import { toLocaleString } from '../../../util/string_utils'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; + +// Used to pass on attribute names to table columns +export enum AnomalyDetectionListColumns { + id = 'id', + maxAnomalyScore = 'max_anomaly_score', + jobIds = 'jobIds', + latestTimestamp = 'latest_timestamp', + docsProcessed = 'docs_processed', + jobsInGroup = 'jobs_in_group', +} + +interface Props { + items: GroupsDictionary; + statsBarData: JobStatsBarStats; + jobsList: MlSummaryJobs; +} + +export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData }) => { + const groupsList = Object.values(items); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const [sortField, setSortField] = useState(AnomalyDetectionListColumns.id); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + // columns: group, max anomaly, jobs in group, latest timestamp, docs processed, action to explorer + const columns: Array> = [ + { + field: AnomalyDetectionListColumns.id, + name: i18n.translate('xpack.ml.overview.anomalyDetection.tableId', { + defaultMessage: 'Group ID', + }), + render: (id: Group['id']) => , + sortable: true, + truncateText: true, + width: '20%', + }, + { + field: AnomalyDetectionListColumns.maxAnomalyScore, + name: ( + + + {i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', { + defaultMessage: 'Max anomaly score', + })}{' '} + + + + ), + sortable: true, + render: (score: Group['max_anomaly_score']) => { + if (score === undefined) { + // score is not loaded yet + return ; + } else if (score === null) { + // an error occurred loading this group's score + return ( + + + + ); + } else if (score === 0) { + return ( + // @ts-ignore + + {score} + + ); + } else { + const color: string = getSeverityColor(score); + return ( + // @ts-ignore + + {score >= 1 ? Math.floor(score) : '< 1'} + + ); + } + }, + truncateText: true, + width: '150px', + }, + { + field: AnomalyDetectionListColumns.jobsInGroup, + name: i18n.translate('xpack.ml.overview.anomalyDetection.tableNumJobs', { + defaultMessage: 'Jobs in group', + }), + sortable: true, + truncateText: true, + width: '100px', + }, + { + field: AnomalyDetectionListColumns.latestTimestamp, + name: i18n.translate('xpack.ml.overview.anomalyDetection.tableLatestTimestamp', { + defaultMessage: 'Latest timestamp', + }), + dataType: 'date', + render: (time: number) => formatHumanReadableDateTimeSeconds(time), + textOnly: true, + truncateText: true, + sortable: true, + width: '20%', + }, + { + field: AnomalyDetectionListColumns.docsProcessed, + name: i18n.translate('xpack.ml.overview.anomalyDetection.tableDocsProcessed', { + defaultMessage: 'Docs processed', + }), + render: (docs: number) => toLocaleString(docs), + textOnly: true, + sortable: true, + width: '20%', + }, + { + name: i18n.translate('xpack.ml.overview.anomalyDetection.tableActionLabel', { + defaultMessage: 'Actions', + }), + render: (group: Group) => , + width: '100px', + align: 'right', + }, + ]; + + const onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: AnomalyDetectionListColumns.id, direction: SORT_DIRECTION.ASC }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + }; + + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: groupsList.length, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }; + + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + const MlInMemoryTable = mlInMemoryTableFactory(); + + return ( + + + + +

+ {i18n.translate('xpack.ml.overview.anomalyDetection.panelTitle', { + defaultMessage: 'Anomaly Detection', + })} +

+
+
+ + + +
+ + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts new file mode 100644 index 0000000000000..01848bad2670e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; +import { Group, GroupsDictionary } from './anomaly_detection_panel'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; + +export function getGroupsFromJobs( + jobs: MlSummaryJobs +): { groups: GroupsDictionary; count: number } { + const groups: any = { + ungrouped: { + id: 'ungrouped', + jobIds: [], + docs_processed: 0, + latest_timestamp: 0, + max_anomaly_score: null, + jobs_in_group: 0, + }, + }; + + jobs.forEach((job: MlSummaryJob) => { + // Organize job by group + if (job.groups.length > 0) { + job.groups.forEach((g: any) => { + if (groups[g] === undefined) { + groups[g] = { + id: g, + jobIds: [job.id], + docs_processed: job.processed_record_count, + latest_timestamp: job.latestTimestampMs, + max_anomaly_score: null, + jobs_in_group: 1, + }; + } else { + groups[g].jobIds.push(job.id); + groups[g].docs_processed += job.processed_record_count; + groups[g].jobs_in_group++; + // if incoming job latest timestamp is greater than the last saved one, replace it + if (groups[g].latest_timestamp === undefined) { + groups[g].latest_timestamp = job.latestTimestampMs; + } else if (job.latestTimestampMs > groups[g].latest_timestamp) { + groups[g].latest_timestamp = job.latestTimestampMs; + } + } + }); + } else { + groups.ungrouped.jobIds.push(job.id); + groups.ungrouped.docs_processed += job.processed_record_count; + groups.ungrouped.jobs_in_group++; + // if incoming job latest timestamp is greater than the last saved one, replace it + if (job.latestTimestampMs > groups.ungrouped.latest_timestamp) { + groups.ungrouped.latest_timestamp = job.latestTimestampMs; + } + } + }); + + if (groups.ungrouped.jobIds.length === 0) { + delete groups.ungrouped; + } + + const count = Object.values(groups).length; + + return { groups, count }; +} + +export function getStatsBarData(jobsList: any) { + const jobStats = { + activeNodes: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', { + defaultMessage: 'Active ML Nodes', + }), + value: 0, + show: true, + }, + total: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', { + defaultMessage: 'Total jobs', + }), + value: 0, + show: true, + }, + open: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.openJobsLabel', { + defaultMessage: 'Open jobs', + }), + value: 0, + show: true, + }, + closed: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.closedJobsLabel', { + defaultMessage: 'Closed jobs', + }), + value: 0, + show: true, + }, + failed: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.failedJobsLabel', { + defaultMessage: 'Failed jobs', + }), + value: 0, + show: false, + }, + activeDatafeeds: { + label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', { + defaultMessage: 'Active datafeeds', + }), + value: 0, + show: true, + }, + }; + + if (jobsList === undefined) { + return jobStats; + } + + // object to keep track of nodes being used by jobs + const mlNodes: any = {}; + let failedJobs = 0; + + jobsList.forEach((job: MlSummaryJob) => { + if (job.jobState === JOB_STATE.OPENED) { + jobStats.open.value++; + } else if (job.jobState === JOB_STATE.CLOSED) { + jobStats.closed.value++; + } else if (job.jobState === JOB_STATE.FAILED) { + failedJobs++; + } + + if (job.hasDatafeed && job.datafeedState === DATAFEED_STATE.STARTED) { + jobStats.activeDatafeeds.value++; + } + + if (job.nodeName !== undefined) { + mlNodes[job.nodeName] = {}; + } + }); + + jobStats.total.value = jobsList.length; + + // Only show failed jobs if it is non-zero + if (failedJobs) { + jobStats.failed.value = failedJobs; + jobStats.failed.show = true; + } else { + jobStats.failed.show = false; + } + + jobStats.activeNodes.value = Object.keys(mlNodes).length; + + return jobStats; +} + +export function getJobsFromGroup(group: Group, jobs: any) { + return group.jobIds.map(jobId => jobs[jobId]).filter(id => id !== undefined); +} + +export function getJobsWithTimerange(jobsList: any) { + const jobs: any = {}; + jobsList.forEach((job: any) => { + if (jobs[job.id] === undefined) { + // create the job in the object with the times you need + if (job.earliestTimestampMs !== undefined) { + const { earliestTimestampMs, latestResultsTimestampMs } = job; + jobs[job.id] = { + id: job.id, + earliestTimestampMs, + latestResultsTimestampMs, + }; + } + } + }); + + return jobs; +} diff --git a/x-pack/legacy/plugins/ml/public/overview/components/content.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/overview/components/content.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx index a285d5c91a266..8d2e4865ee6f4 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/content.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AnomalyDetectionPanel } from './anomaly_detection_panel'; -import { AnalyticsPanel } from './analytics_panel/'; +import { AnalyticsPanel } from './analytics_panel'; interface Props { createAnomalyDetectionJobDisabled: boolean; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/directive.tsx b/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/index.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx b/x-pack/legacy/plugins/ml/public/application/overview/overview_page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/overview_page.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/overview_page.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/route.ts b/x-pack/legacy/plugins/ml/public/application/overview/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/route.ts rename to x-pack/legacy/plugins/ml/public/application/overview/route.ts diff --git a/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts rename to x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts index 2c0cb71cd876c..6cc06231a08d0 100644 --- a/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { hasLicenseExpired } from '../license/check_license'; -import { Privileges, getDefaultPrivileges } from '../../common/types/privileges'; +import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; import { getPrivileges, getManageMlPrivileges } from './get_privileges'; import { ACCESS_DENIED_PATH } from '../management/management_urls'; diff --git a/x-pack/legacy/plugins/ml/public/privilege/get_privileges.ts b/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts similarity index 93% rename from x-pack/legacy/plugins/ml/public/privilege/get_privileges.ts rename to x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts index adca02b434e38..a3811779333d9 100644 --- a/x-pack/legacy/plugins/ml/public/privilege/get_privileges.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts @@ -7,7 +7,7 @@ import { ml } from '../services/ml_api_service'; import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../common/types/privileges'; +import { PrivilegesResponse } from '../../../common/types/privileges'; export function getPrivileges(): Promise { return new Promise((resolve, reject) => { diff --git a/x-pack/legacy/plugins/ml/public/services/__mocks__/cloudwatch_job_caps_response.json b/x-pack/legacy/plugins/ml/public/application/services/__mocks__/cloudwatch_job_caps_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/__mocks__/cloudwatch_job_caps_response.json rename to x-pack/legacy/plugins/ml/public/application/services/__mocks__/cloudwatch_job_caps_response.json diff --git a/x-pack/legacy/plugins/ml/public/services/__mocks__/ml_info_response.json b/x-pack/legacy/plugins/ml/public/application/services/__mocks__/ml_info_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/__mocks__/ml_info_response.json rename to x-pack/legacy/plugins/ml/public/application/services/__mocks__/ml_info_response.json diff --git a/x-pack/legacy/plugins/ml/public/services/annotations_service.test.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/services/annotations_service.test.tsx rename to x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx index 3e2410292629b..eed9e46a47745 100644 --- a/x-pack/legacy/plugins/ml/public/services/annotations_service.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx @@ -6,7 +6,7 @@ import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json'; -import { Annotation } from '../../common/types/annotations'; +import { Annotation } from '../../../common/types/annotations'; import { annotation$, annotationsRefresh$ } from './annotations_service'; describe('annotations_service', () => { diff --git a/x-pack/legacy/plugins/ml/public/services/annotations_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx similarity index 96% rename from x-pack/legacy/plugins/ml/public/services/annotations_service.tsx rename to x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx index 4e8b0ad99d371..051c6ab445102 100644 --- a/x-pack/legacy/plugins/ml/public/services/annotations_service.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx @@ -6,7 +6,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; -import { Annotation } from '../../common/types/annotations'; +import { Annotation } from '../../../common/types/annotations'; /* A TypeScript helper type to allow a given component state attribute to be either an annotation or null. @@ -41,7 +41,7 @@ export type AnnotationState = Annotation | null; There are two ways to deal with updates of the observable: - 1. Inline subscription in an existing component. + 1. Inline subscription in an existing component. This requires the component to be a class component and manage its own state. - To react to an update, use `annotation$.subscribe(annotation => { })`. diff --git a/x-pack/legacy/plugins/ml/public/services/calendar_service.js b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js similarity index 93% rename from x-pack/legacy/plugins/ml/public/services/calendar_service.js rename to x-pack/legacy/plugins/ml/public/application/services/calendar_service.js index 09e3001a0f7f5..dafb6b49ad14d 100644 --- a/x-pack/legacy/plugins/ml/public/services/calendar_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js @@ -9,9 +9,9 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar'; +import { ml } from '../services/ml_api_service'; +import { mlJobService } from '../services/job_service'; +import { mlMessageBarService } from '../components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/services/field_format_service.ts b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/services/field_format_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts index e4341adebda7a..ce6bc7896c44c 100644 --- a/x-pack/legacy/plugins/ml/public/services/field_format_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts @@ -5,9 +5,9 @@ */ import { IndexPattern } from 'ui/index_patterns'; -import { mlFunctionToESAggregation } from '../../common/util/job_utils'; +import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; import { getIndexPatternById, getIndexPatternIdFromName } from '../util/index_utils'; -import { mlJobService } from '../services/job_service'; +import { mlJobService } from './job_service'; type FormatsByJobId = Record; type IndexPatternIdsByJob = Record; diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts new file mode 100644 index 0000000000000..19f77d97a5708 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +export interface ForecastData { + success: boolean; + results: any; +} + +export const mlForecastService: { + getForecastData: ( + job: Job, + detectorIndex: number, + forecastId: string, + entityFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType: any + ) => Observable; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js new file mode 100644 index 0000000000000..4b6ce19b5e6c6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js @@ -0,0 +1,378 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +// Service for carrying out requests to run ML forecasts and to obtain +// data on forecasts that have been performed. +import _ from 'lodash'; +import { map } from 'rxjs/operators'; + +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ml } from './ml_api_service'; + +// Gets a basic summary of the most recently run forecasts for the specified +// job, with results at or later than the supplied timestamp. +// Extra query object can be supplied, or pass null if no additional query. +// Returned response contains a forecasts property, which is an array of objects +// containing id, earliest and latest keys. +function getForecastsSummary( + job, + query, + earliestMs, + maxResults +) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + forecasts: [] + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, result type and earliest time, plus + // the additional query if supplied. + const filterCriteria = [ + { + term: { result_type: 'model_forecast_request_stats' } + }, + { + term: { job_id: job.job_id } + }, + { + range: { + timestamp: { + gte: earliestMs, + format: 'epoch_millis' + } + } + } + ]; + + if (query) { + filterCriteria.push(query); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: maxResults, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: filterCriteria + } + }, + sort: [ + { forecast_create_timestamp: { 'order': 'desc' } } + ] + } + }) + .then((resp) => { + if (resp.hits.total !== 0) { + obj.forecasts = resp.hits.hits.map(hit => hit._source); + } + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Obtains the earliest and latest timestamps for the forecast data from +// the forecast with the specified ID. +// Returned response contains earliest and latest properties which are the +// timestamps of the first and last model_forecast results. +function getForecastDateRange(job, forecastId) { + + return new Promise((resolve, reject) => { + const obj = { + success: true, + earliest: null, + latest: null + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, result type and time range. + const filterCriteria = [{ + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true + } + }, + { + term: { job_id: job.job_id } + }, + { + term: { forecast_id: forecastId } + }]; + + // TODO - add in criteria for detector index and entity fields (by, over, partition) + // once forecasting with these parameters is supported. + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: filterCriteria + } + }, + aggs: { + earliest: { + min: { + field: 'timestamp' + } + }, + latest: { + max: { + field: 'timestamp' + } + } + } + } + }) + .then((resp) => { + obj.earliest = _.get(resp, 'aggregations.earliest.value', null); + obj.latest = _.get(resp, 'aggregations.latest.value', null); + if (obj.earliest === null || obj.latest === null) { + reject(resp); + } else { + resolve(obj); + } + }) + .catch((resp) => { + reject(resp); + }); + + }); +} + +// Obtains the requested forecast model data for the forecast with the specified ID. +function getForecastData( + job, + detectorIndex, + forecastId, + entityFields, + earliestMs, + latestMs, + interval, + aggType) { + // Extract the partition, by, over fields on which to filter. + const criteriaFields = []; + const detector = job.analysis_config.detectors[detectorIndex]; + if (_.has(detector, 'partition_field_name')) { + const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); + if (partitionEntity !== undefined) { + criteriaFields.push( + { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); + } + } + + if (_.has(detector, 'over_field_name')) { + const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); + if (overEntity !== undefined) { + criteriaFields.push( + { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); + } + } + + if (_.has(detector, 'by_field_name')) { + const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); + if (byEntity !== undefined) { + criteriaFields.push( + { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); + } + } + + const obj = { + success: true, + results: {} + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, detector index, result type and time range. + const filterCriteria = [{ + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true + } + }, + { + term: { job_id: job.job_id } + }, + { + term: { forecast_id: forecastId } + }, + { + term: { detector_index: detectorIndex } + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }]; + + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, (criteria) => { + filterCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue + } + }); + }); + + + + // If an aggType object has been passed in, use it. + // Otherwise default to avg, min and max aggs for the + // forecast prediction, upper and lower + const forecastAggs = (aggType === undefined) ? + { avg: 'avg', max: 'max', min: 'min' } : + { + avg: aggType.avg, + max: aggType.max, + min: aggType.min + }; + + return ml.esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: filterCriteria + } + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval: interval, + min_doc_count: 1 + }, + aggs: { + prediction: { + [forecastAggs.avg]: { + field: 'forecast_prediction' + } + }, + forecastUpper: { + [forecastAggs.max]: { + field: 'forecast_upper' + } + }, + forecastLower: { + [forecastAggs.min]: { + field: 'forecast_lower' + } + } + } + } + } + } + }).pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = { + prediction: _.get(dataForTime, ['prediction', 'value']), + forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), + forecastLower: _.get(dataForTime, ['forecastLower', 'value']) + }; + }); + + return obj; + }) + ); +} + +// Runs a forecast +function runForecast(jobId, duration) { + console.log('ML forecast service run forecast with duration:', duration); + return new Promise((resolve, reject) => { + + ml.forecast({ + jobId, + duration + }) + .then((resp) => { + resolve(resp); + }).catch((err) => { + reject(err); + }); + }); +} + +// Gets stats for a forecast that has been run on the specified job. +// Returned response contains a stats property, including +// forecast_progress (a value from 0 to 1), +// and forecast_status ('finished' when complete) properties. +function getForecastRequestStats(job, forecastId) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + stats: {} + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, result type and earliest time. + const filterCriteria = [{ + query_string: { + query: 'result_type:model_forecast_request_stats', + analyze_wildcard: true + } + }, + { + term: { job_id: job.job_id } + }, + { + term: { forecast_id: forecastId } + }]; + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 1, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: filterCriteria + } + } + } + }) + .then((resp) => { + if (resp.hits.total !== 0) { + obj.stats = _.first(resp.hits.hits)._source; + } + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + + }); +} + +export const mlForecastService = { + getForecastsSummary, + getForecastDateRange, + getForecastData, + runForecast, + getForecastRequestStats +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts new file mode 100644 index 0000000000000..1d68ec5b886eb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// service for interacting with the server + +import chrome from 'ui/chrome'; + +// @ts-ignore +import { addSystemApiHeader } from 'ui/system_api'; +import { fromFetch } from 'rxjs/fetch'; +import { from, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +export interface HttpOptions { + url?: string; +} + +function getResultHeaders(headers: HeadersInit): HeadersInit { + return addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...headers, + }); +} + +export function http(options: any) { + return new Promise((resolve, reject) => { + if (options && options.url) { + let url = ''; + url = url + (options.url || ''); + const headers: Record = addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...options.headers, + }); + + const allHeaders = + options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: RequestInit = { + method: options.method || 'GET', + headers: allHeaders, + credentials: 'same-origin', + }; + + if (body !== null) { + payload.body = body; + } + + fetch(url, payload) + .then(resp => { + resp.json().then(resp.ok === true ? resolve : reject); + }) + .catch(resp => { + reject(resp); + }); + } else { + reject(); + } + }); +} + +interface RequestOptions extends RequestInit { + body: BodyInit | any; +} + +export function http$(url: string, options: RequestOptions): Observable { + const requestInit: RequestInit = { + ...options, + credentials: 'same-origin', + method: options.method || 'GET', + ...(options.body ? { body: JSON.stringify(options.body) as string } : {}), + headers: getResultHeaders(options.headers ?? {}), + }; + + return fromFetch(url, requestInit).pipe( + switchMap(response => { + if (response.ok) { + return from(response.json() as Promise); + } else { + throw new Error(String(response.status)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/services/job_messages_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/services/job_messages_service.js rename to x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js index 725de9e9c6237..fcd4d6088b44b 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_messages_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js @@ -10,8 +10,8 @@ // Service for carrying out Elasticsearch queries to obtain data for the // Ml Results dashboards. -import { ML_NOTIFICATION_INDEX_PATTERN } from 'plugins/ml/../common/constants/index_patterns'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ml } from '../services/ml_api_service'; // filter to match job_type: 'anomaly_detector' or no job_type field at all // if no job_type field exist, we can assume the message is for an anomaly detector job diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/job_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_service.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/services/job_service.js rename to x-pack/legacy/plugins/ml/public/application/services/job_service.js index 27dcd0135ad77..3db2b6c6dd88e 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.js @@ -15,8 +15,8 @@ import { ml } from './ml_api_service'; import { mlMessageBarService } from '../components/messagebar'; import { isWebUrl } from '../util/url_utils'; -import { ML_DATA_PREVIEW_COUNT } from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; +import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; +import { parseInterval } from '../../../common/util/parse_interval'; const msgs = mlMessageBarService; let jobs = []; diff --git a/x-pack/legacy/plugins/ml/public/services/mapping_service.js b/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js similarity index 95% rename from x-pack/legacy/plugins/ml/public/services/mapping_service.js rename to x-pack/legacy/plugins/ml/public/application/services/mapping_service.js index c206930e12cd6..3ee49fda20819 100644 --- a/x-pack/legacy/plugins/ml/public/services/mapping_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js @@ -8,7 +8,7 @@ import _ from 'lodash'; -import { ml } from '../services/ml_api_service'; +import { ml } from './ml_api_service'; // Returns the mapping type of the specified field. // Accepts fieldName containing dots representing a nested sub-field. diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts new file mode 100644 index 0000000000000..54d55159646f6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; + +import { Annotation } from '../../../../common/types/annotations'; +import { http, http$ } from '../http_service'; + +const basePath = chrome.addBasePath('/api/ml'); + +export const annotations = { + getAnnotations(obj: { + jobIds: string[]; + earliestMs: number; + latestMs: number; + maxAnnotations: number; + }) { + return http$<{ annotations: Record }>(`${basePath}/annotations`, { + method: 'POST', + body: obj, + }); + }, + indexAnnotation(obj: any) { + return http({ + url: `${basePath}/annotations/index`, + method: 'PUT', + data: obj, + }); + }, + deleteAnnotation(id: string) { + return http({ + url: `${basePath}/annotations/delete/${id}`, + method: 'DELETE', + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/data_frame_analytics.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js index 3f987a1763140..d29793366b9a2 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/data_frame_analytics.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js similarity index 95% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/datavisualizer.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js index 323a70a6912b4..eb4c84ce5764c 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/datavisualizer.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js @@ -6,7 +6,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js new file mode 100644 index 0000000000000..18b7d93b0ca2e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Service for querying filters, which hold lists of entities, +// for example a list of known safe URL domains. + +import chrome from 'ui/chrome'; + +import { http } from '../http_service'; + +const basePath = chrome.addBasePath('/api/ml'); + +export const filters = { + filters(obj) { + const filterId = (obj && obj.filterId) ? `/${obj.filterId}` : ''; + return http({ + url: `${basePath}/filters${filterId}`, + method: 'GET' + }); + }, + + filtersStats() { + return http({ + url: `${basePath}/filters/_stats`, + method: 'GET' + }); + }, + + addFilter( + filterId, + description, + items) { + return http({ + url: `${basePath}/filters`, + method: 'PUT', + data: { + filterId, + description, + items + } + }); + }, + + updateFilter( + filterId, + description, + addItems, + removeItems + ) { + const data = {}; + if (description !== undefined) { + data.description = description; + } + if (addItems !== undefined) { + data.addItems = addItems; + } + if (removeItems !== undefined) { + data.removeItems = removeItems; + } + + return http({ + url: `${basePath}/filters/${filterId}`, + method: 'PUT', + data + }); + }, + + deleteFilter(filterId) { + return http({ + url: `${basePath}/filters/${filterId}`, + method: 'DELETE' + }); + }, + + +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts new file mode 100644 index 0000000000000..7c0b22b0e1966 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Observable } from 'rxjs'; +import { Annotation } from '../../../../common/types/annotations'; +import { AggFieldNamePair } from '../../../../common/types/fields'; +import { ExistingJobsAndGroups } from '../job_service'; +import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlSummaryJobs } from '../../../../common/types/jobs'; +import { MlServerDefaults, MlServerLimits } from '../ml_server_info'; +import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { JobMessage } from '../../../../common/types/audit_message'; +import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; +import { DeepPartial } from '../../../../common/types/common'; +import { annotations } from './annotations'; + +// TODO This is not a complete representation of all methods of `ml.*`. +// It just satisfies needs for other parts of the code area which use +// TypeScript and rely on the methods typed in here. +// This allows the import of `ml` into TypeScript code. +interface EsIndex { + name: string; +} + +export interface GetTimeFieldRangeResponse { + success: boolean; + start: { epoch: number; string: string }; + end: { epoch: number; string: string }; +} + +export interface BucketSpanEstimatorData { + aggTypes: Array; + duration: { + start: number; + end: number; + }; + fields: Array; + index: string; + query: any; + splitField: string | undefined; + timeField: string | undefined; +} + +export interface BucketSpanEstimatorResponse { + name: string; + ms: number; + error?: boolean; + message?: { msg: string } | string; +} + +export interface MlInfoResponse { + defaults: MlServerDefaults; + limits: MlServerLimits; + native_code: { + build_hash: string; + version: string; + }; + upgrade_mode: boolean; + cloudId?: string; +} + +declare interface Ml { + annotations: { + deleteAnnotation(id: string | undefined): Promise; + indexAnnotation(annotation: Annotation): Promise; + getAnnotations: typeof annotations.getAnnotations; + }; + + dataFrameAnalytics: { + getDataFrameAnalytics(analyticsId?: string): Promise; + getDataFrameAnalyticsStats(analyticsId?: string): Promise; + createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise; + evaluateDataFrameAnalytics(evaluateConfig: any): Promise; + estimateDataFrameAnalyticsMemoryUsage( + jobConfig: DeepPartial + ): Promise; + deleteDataFrameAnalytics(analyticsId: string): Promise; + startDataFrameAnalytics(analyticsId: string): Promise; + stopDataFrameAnalytics( + analyticsId: string, + force?: boolean, + waitForCompletion?: boolean + ): Promise; + getAnalyticsAuditMessages(analyticsId: string): Promise; + }; + + hasPrivileges(obj: object): Promise; + + checkMlPrivileges(): Promise; + checkManageMLPrivileges(): Promise; + getJobStats(obj: object): Promise; + getDatafeedStats(obj: object): Promise; + esSearch(obj: object): any; + esSearch$(obj: object): Observable; + getIndices(): Promise; + dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; + getDataRecognizerModule(obj: { moduleId: string }): Promise; + setupDataRecognizerConfig(obj: object): Promise; + getTimeFieldRange(obj: object): Promise; + calculateModelMemoryLimit(obj: object): Promise<{ modelMemoryLimit: string }>; + calendars(): Promise< + Array<{ + calendar_id: string; + description: string; + events: any[]; + job_ids: string[]; + }> + >; + + getVisualizerFieldStats(obj: object): Promise; + getVisualizerOverallStats(obj: object): Promise; + + results: { + getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise; + }; + + jobs: { + jobsSummary(jobIds: string[]): Promise; + jobs(jobIds: string[]): Promise; + groups(): Promise; + updateGroups(updatedJobs: string[]): Promise; + forceStartDatafeeds(datafeedIds: string[], start: string, end: string): Promise; + stopDatafeeds(datafeedIds: string[]): Promise; + deleteJobs(jobIds: string[]): Promise; + closeJobs(jobIds: string[]): Promise; + jobAuditMessages(jobId: string, from?: string): Promise; + deletingJobTasks(): Promise; + newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; + newJobLineChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null, + splitFieldValue: string | null + ): Promise; + newJobPopulationsChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string + ): Promise; + getAllJobAndGroupIds(): Promise; + getLookBackProgress( + jobId: string, + start: number, + end: number + ): Promise<{ progress: number; isRunning: boolean; isJobClosed: boolean }>; + }; + + estimateBucketSpan(data: BucketSpanEstimatorData): Promise; + + mlNodeCount(): Promise<{ count: number }>; + mlInfo(): Promise; + getCardinalityOfFields(obj: Record): any; +} + +declare const ml: Ml; + +export interface GetDataFrameAnalyticsStatsResponseOk { + node_failures?: object; + count: number; + data_frame_analytics: DataFrameAnalyticsStats[]; +} + +export interface GetDataFrameAnalyticsStatsResponseError { + statusCode: number; + error: string; + message: string; +} + +export type GetDataFrameAnalyticsStatsResponse = + | GetDataFrameAnalyticsStatsResponseOk + | GetDataFrameAnalyticsStatsResponseError; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js new file mode 100644 index 0000000000000..34d9f9ec16f83 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -0,0 +1,468 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { pick } from 'lodash'; +import chrome from 'ui/chrome'; + +import { http, http$ } from '../http_service'; + +import { annotations } from './annotations'; +import { dataFrameAnalytics } from './data_frame_analytics'; +import { filters } from './filters'; +import { results } from './results'; +import { jobs } from './jobs'; +import { fileDatavisualizer } from './datavisualizer'; + +const basePath = chrome.addBasePath('/api/ml'); + +export const ml = { + getJobs(obj) { + const jobId = (obj && obj.jobId) ? `/${obj.jobId}` : ''; + return http({ + url: `${basePath}/anomaly_detectors${jobId}`, + }); + }, + + getJobStats(obj) { + const jobId = (obj && obj.jobId) ? `/${obj.jobId}` : ''; + return http({ + url: `${basePath}/anomaly_detectors${jobId}/_stats`, + }); + }, + + addJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}`, + method: 'PUT', + data: obj.job + }); + }, + + openJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}/_open`, + method: 'POST' + }); + }, + + closeJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}/_close`, + method: 'POST' + }); + }, + + deleteJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}`, + method: 'DELETE' + }); + }, + + forceDeleteJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}?force=true`, + method: 'DELETE' + }); + }, + + updateJob(obj) { + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}/_update`, + method: 'POST', + data: obj.job + }); + }, + + estimateBucketSpan(obj) { + return http({ + url: `${basePath}/validate/estimate_bucket_span`, + method: 'POST', + data: obj + }); + }, + + validateJob(obj) { + return http({ + url: `${basePath}/validate/job`, + method: 'POST', + data: obj + }); + }, + + validateCardinality(obj) { + return http({ + url: `${basePath}/validate/cardinality`, + method: 'POST', + data: obj + }); + }, + + getDatafeeds(obj) { + const datafeedId = (obj && obj.datafeedId) ? `/${obj.datafeedId}` : ''; + return http({ + url: `${basePath}/datafeeds${datafeedId}`, + }); + }, + + getDatafeedStats(obj) { + const datafeedId = (obj && obj.datafeedId) ? `/${obj.datafeedId}` : ''; + return http({ + url: `${basePath}/datafeeds${datafeedId}/_stats`, + }); + }, + + addDatafeed(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}`, + method: 'PUT', + data: obj.datafeedConfig + }); + }, + + updateDatafeed(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}/_update`, + method: 'POST', + data: obj.datafeedConfig + }); + }, + + deleteDatafeed(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}`, + method: 'DELETE' + }); + }, + + forceDeleteDatafeed(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}?force=true`, + method: 'DELETE' + }); + }, + + startDatafeed(obj) { + const data = {}; + if(obj.start !== undefined) { + data.start = obj.start; + } + if(obj.end !== undefined) { + data.end = obj.end; + } + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}/_start`, + method: 'POST', + data + }); + }, + + stopDatafeed(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}/_stop`, + method: 'POST' + }); + }, + + datafeedPreview(obj) { + return http({ + url: `${basePath}/datafeeds/${obj.datafeedId}/_preview`, + method: 'GET' + }); + }, + + validateDetector(obj) { + return http({ + url: `${basePath}/anomaly_detectors/_validate/detector`, + method: 'POST', + data: obj.detector + }); + }, + + forecast(obj) { + const data = {}; + if(obj.duration !== undefined) { + data.duration = obj.duration; + } + + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}/_forecast`, + method: 'POST', + data + }); + }, + + overallBuckets(obj) { + const data = pick(obj, [ + 'topN', + 'bucketSpan', + 'start', + 'end' + ]); + return http({ + url: `${basePath}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, + method: 'POST', + data + }); + }, + + hasPrivileges(obj) { + return http({ + url: `${basePath}/_has_privileges`, + method: 'POST', + data: obj + }); + }, + + checkMlPrivileges() { + return http({ + url: `${basePath}/ml_capabilities`, + method: 'GET', + }); + }, + + checkManageMLPrivileges() { + return http({ + url: `${basePath}/ml_capabilities?ignoreSpaces=true`, + method: 'GET' + }); + }, + + getNotificationSettings() { + return http({ + url: `${basePath}/notification_settings`, + method: 'GET' + }); + }, + + getFieldCaps(obj) { + const data = {}; + if(obj.index !== undefined) { + data.index = obj.index; + } + if(obj.fields !== undefined) { + data.fields = obj.fields; + } + return http({ + url: `${basePath}/indices/field_caps`, + method: 'POST', + data + }); + }, + + recognizeIndex(obj) { + return http({ + url: `${basePath}/modules/recognize/${obj.indexPatternTitle}`, + method: 'GET' + }); + }, + + listDataRecognizerModules() { + return http({ + url: `${basePath}/modules/get_module`, + method: 'GET' + }); + }, + + getDataRecognizerModule(obj) { + return http({ + url: `${basePath}/modules/get_module/${obj.moduleId}`, + method: 'GET' + }); + }, + + dataRecognizerModuleJobsExist(obj) { + return http({ + url: `${basePath}/modules/jobs_exist/${obj.moduleId}`, + method: 'GET' + }); + }, + + setupDataRecognizerConfig(obj) { + const data = pick(obj, [ + 'prefix', + 'groups', + 'indexPatternName', + 'query', + 'useDedicatedIndex', + 'startDatafeed', + 'start', + 'end', + 'jobOverrides', + ]); + + return http({ + url: `${basePath}/modules/setup/${obj.moduleId}`, + method: 'POST', + data + }); + }, + + getVisualizerFieldStats(obj) { + const data = pick(obj, [ + 'query', + 'timeFieldName', + 'earliest', + 'latest', + 'samplerShardSize', + 'interval', + 'fields', + 'maxExamples' + ]); + + return http({ + url: `${basePath}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, + method: 'POST', + data + }); + }, + + getVisualizerOverallStats(obj) { + const data = pick(obj, [ + 'query', + 'timeFieldName', + 'earliest', + 'latest', + 'samplerShardSize', + 'aggregatableFields', + 'nonAggregatableFields' + ]); + + return http({ + url: `${basePath}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, + method: 'POST', + data + }); + }, + + calendars(obj) { + const calendarId = (obj && obj.calendarId) ? `/${obj.calendarId}` : ''; + return http({ + url: `${basePath}/calendars${calendarId}`, + method: 'GET' + }); + }, + + addCalendar(obj) { + return http({ + url: `${basePath}/calendars`, + method: 'PUT', + data: obj + }); + }, + + updateCalendar(obj) { + const calendarId = (obj && obj.calendarId) ? `/${obj.calendarId}` : ''; + return http({ + url: `${basePath}/calendars${calendarId}`, + method: 'PUT', + data: obj + }); + }, + + deleteCalendar(obj) { + return http({ + url: `${basePath}/calendars/${obj.calendarId}`, + method: 'DELETE' + }); + }, + + mlNodeCount() { + return http({ + url: `${basePath}/ml_node_count`, + method: 'GET' + }); + }, + + mlInfo() { + return http({ + url: `${basePath}/info`, + method: 'GET' + }); + }, + + calculateModelMemoryLimit(obj) { + const data = pick(obj, [ + 'indexPattern', + 'splitFieldName', + 'query', + 'fieldNames', + 'influencerNames', + 'timeFieldName', + 'earliestMs', + 'latestMs' + ]); + + return http({ + url: `${basePath}/validate/calculate_model_memory_limit`, + method: 'POST', + data + }); + }, + + getCardinalityOfFields(obj) { + const data = pick(obj, [ + 'index', + 'fieldNames', + 'query', + 'timeFieldName', + 'earliestMs', + 'latestMs' + ]); + + return http({ + url: `${basePath}/fields_service/field_cardinality`, + method: 'POST', + data + }); + }, + + getTimeFieldRange(obj) { + const data = pick(obj, [ + 'index', + 'timeFieldName', + 'query' + ]); + + return http({ + url: `${basePath}/fields_service/time_field_range`, + method: 'POST', + data + }); + }, + + esSearch(obj) { + return http({ + url: `${basePath}/es_search`, + method: 'POST', + data: obj + }); + }, + + esSearch$(obj) { + return http$(`${basePath}/es_search`, { + method: 'POST', + body: obj + }); + }, + + getIndices() { + const tempBasePath = chrome.addBasePath('/api'); + return http({ + url: `${tempBasePath}/index_management/indices`, + method: 'GET', + }); + }, + + annotations, + dataFrameAnalytics, + filters, + results, + jobs, + fileDatavisualizer, +}; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index 39b646998b426..4ff1ca785d226 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -6,7 +6,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js similarity index 92% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/results.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 7a776d61dca21..f9874cca840a7 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http, http$ } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); @@ -25,11 +25,9 @@ export const results = { maxRecords, maxExamples, influencersFilterQuery) { - - return http({ - url: `${basePath}/results/anomalies_table_data`, + return http$(`${basePath}/results/anomalies_table_data`, { method: 'POST', - data: { + body: { jobIds, criteriaFields, influencers, diff --git a/x-pack/legacy/plugins/ml/public/services/ml_server_info.test.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_server_info.test.ts rename to x-pack/legacy/plugins/ml/public/application/services/ml_server_info.test.ts diff --git a/x-pack/legacy/plugins/ml/public/services/ml_server_info.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_server_info.ts rename to x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities._service.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts rename to x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities._service.test.ts diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts similarity index 98% rename from x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index ded9aa410766e..aeec71462308e 100644 --- a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -14,8 +14,8 @@ import { FieldId, NewJobCaps, EVENT_RATE_FIELD_ID, -} from '../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +} from '../../../common/types/fields'; +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { ml } from './ml_api_service'; // called in the angular routing resolve block to initialize the @@ -38,7 +38,7 @@ export function loadNewJobCapabilities( // saved search is being used // load the index pattern from the saved search const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index'); + const indexPattern = savedSearch.searchSource.getField('index')!; await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts new file mode 100644 index 0000000000000..9ab14aa7495a7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getMetricData, + getModelPlotOutput, + getRecordsForCriteria, + getScheduledEventsByBucket, +} from './result_service_rx'; +import { + getEventDistributionData, + getEventRateData, + getInfluencerValueMaxScoreByTime, + getOverallBucketScores, + getRecordInfluencers, + getRecordMaxScoreByTime, + getRecords, + getRecordsForDetector, + getRecordsForInfluencer, + getScoresByBucket, + getTopInfluencers, + getTopInfluencerValues, +} from './results_service'; + +export const mlResultsService = { + getScoresByBucket, + getScheduledEventsByBucket, + getTopInfluencers, + getTopInfluencerValues, + getOverallBucketScores, + getInfluencerValueMaxScoreByTime, + getRecordInfluencers, + getRecordsForInfluencer, + getRecordsForDetector, + getRecords, + getRecordsForCriteria, + getMetricData, + getEventRateData, + getEventDistributionData, + getModelPlotOutput, + getRecordMaxScoreByTime, +}; + +type time = string; +export interface ModelPlotOutputResults { + results: Record; +} + +export interface CriteriaField { + fieldName: string; + fieldValue: any; +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts new file mode 100644 index 0000000000000..2341ae15a3378 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -0,0 +1,534 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Queries Elasticsearch to obtain metric aggregation results. +// index can be a String, or String[], of index names to search. +// entityFields parameter must be an array, with each object in the array having 'fieldName' +// and 'fieldValue' properties. +// Extra query object can be supplied, or pass null if no additional query +// to that built from the supplied entity fields. +// Returned response contains a results property containing the requested aggregation. +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import _ from 'lodash'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { ml } from '../ml_api_service'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; +import { CriteriaField } from './index'; + +interface ResultResponse { + success: boolean; +} + +export interface MetricData extends ResultResponse { + results: Record; +} + +export function getMetricData( + index: string, + entityFields: any[], + query: object | undefined, + metricFunction: string, // ES aggregation name + metricFieldName: string, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string +): Observable { + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, entity fields, + // plus any additional supplied query. + const shouldCriteria: object[] = []; + const mustCriteria: object[] = [ + { + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ...(query ? [query] : []), + ]; + + entityFields.forEach(entity => { + if (entity.fieldValue.length !== 0) { + mustCriteria.push({ + term: { + [entity.fieldName]: entity.fieldValue, + }, + }); + } else { + // Add special handling for blank entity field values, checking for either + // an empty string or the field not existing. + shouldCriteria.push({ + bool: { + must: [ + { + term: { + [entity.fieldName]: '', + }, + }, + ], + }, + }); + shouldCriteria.push({ + bool: { + must_not: [ + { + exists: { field: entity.fieldName }, + }, + ], + }, + }); + } + }); + + const body: any = { + query: { + bool: { + must: mustCriteria, + }, + }, + size: 0, + _source: { + excludes: [], + }, + aggs: { + byTime: { + date_histogram: { + field: timeFieldName, + interval, + min_doc_count: 0, + }, + }, + }, + }; + + if (shouldCriteria.length > 0) { + body.query.bool.should = shouldCriteria; + body.query.bool.minimum_should_match = shouldCriteria.length / 2; + } + + if (metricFieldName !== undefined && metricFieldName !== '') { + body.aggs.byTime.aggs = {}; + + const metricAgg: any = { + [metricFunction]: { + field: metricFieldName, + }, + }; + + if (metricFunction === 'percentiles') { + metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; + } + body.aggs.byTime.aggs.metric = metricAgg; + } + + return ml.esSearch$({ index, body }).pipe( + map((resp: any) => { + const obj: MetricData = { success: true, results: {} }; + const dataByTime = resp?.aggregations?.byTime?.buckets ?? []; + dataByTime.forEach((dataForTime: any) => { + if (metricFunction === 'count') { + obj.results[dataForTime.key] = dataForTime.doc_count; + } else { + const value = dataForTime?.metric?.value; + const values = dataForTime?.metric?.values; + if (dataForTime.doc_count === 0) { + obj.results[dataForTime.key] = null; + } else if (value !== undefined) { + obj.results[dataForTime.key] = value; + } else if (values !== undefined) { + // Percentiles agg currently returns NaN rather than null when none of the docs in the + // bucket contain the field used in the aggregation + // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). + // Store as null, so values can be handled in the same manner downstream as other aggs + // (min, mean, max) which return null. + const medianValues = values[ML_MEDIAN_PERCENTS]; + obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; + } else { + obj.results[dataForTime.key] = null; + } + } + }); + + return obj; + }) + ); +} + +export interface ModelPlotOutput extends ResultResponse { + results: Record; +} + +export function getModelPlotOutput( + jobId: string, + detectorIndex: number, + criteriaFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType?: { min: any; max: any } +): Observable { + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; + + // if an aggType object has been passed in, use it. + // otherwise default to min and max aggs for the upper and lower bounds + const modelAggs = + aggType === undefined + ? { max: 'max', min: 'min' } + : { + max: aggType.max, + min: aggType.min, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID and time range. + const mustCriteria: object[] = [ + { + term: { job_id: jobId }, + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ]; + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + // Add criteria for the detector index. Results from jobs created before 6.1 will not + // contain a detector_index field, so use a should criteria with a 'not exists' check. + const shouldCriteria = [ + { + term: { detector_index: detectorIndex }, + }, + { + bool: { + must_not: [ + { + exists: { field: 'detector_index' }, + }, + ], + }, + }, + ]; + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:model_plot', + analyze_wildcard: true, + }, + }, + { + bool: { + must: mustCriteria, + should: shouldCriteria, + minimum_should_match: 1, + }, + }, + ], + }, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 0, + }, + aggs: { + actual: { + avg: { + field: 'actual', + }, + }, + modelUpper: { + [modelAggs.max]: { + field: 'model_upper', + }, + }, + modelLower: { + [modelAggs.min]: { + field: 'model_lower', + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime: any) => { + const time = dataForTime.key; + const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']); + const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']); + const actual = _.get(dataForTime, ['actual', 'value']); + + obj.results[time] = { + actual, + modelUpper: + modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper, + modelLower: + modelLower === undefined || isFinite(modelLower) === false ? null : modelLower, + }; + }); + + return obj; + }) + ); +} + +export interface RecordsForCriteria extends ResultResponse { + records: any[]; +} + +// Queries Elasticsearch to obtain the record level results matching the given criteria, +// for the specified job(s), time range, and record score threshold. +// criteriaFields parameter must be an array, with each object in the array having 'fieldName' +// 'fieldValue' properties. +// Pass an empty array or ['*'] to search over all job IDs. +export function getRecordsForCriteria( + jobIds: string[] | undefined, + criteriaFields: CriteriaField[], + threshold: any, + earliestMs: number, + latestMs: number, + maxResults: number | undefined +): Observable { + const obj: RecordsForCriteria = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + boolCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + rest_total_hits_as_int: true, + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }) + .pipe( + map(resp => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit: any) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); +} + +export interface ScheduledEventsByBucket extends ResultResponse { + events: Record; +} + +// Obtains a list of scheduled events by job ID and time. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a events property, which will only +// contains keys for jobs which have scheduled events for the specified time range. +export function getScheduledEventsByBucket( + jobIds: string[] | undefined, + earliestMs: number, + latestMs: number, + interval: string, + maxJobs: number, + maxEvents: number +): Observable { + const obj: ScheduledEventsByBucket = { + success: true, + events: {}, + }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + exists: { field: 'scheduled_events' }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:bucket', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + aggs: { + jobs: { + terms: { + field: 'job_id', + min_doc_count: 1, + size: maxJobs, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1, + }, + aggs: { + events: { + terms: { + field: 'scheduled_events', + size: maxEvents, + }, + }, + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); + _.each(dataByJobId, (dataForJob: any) => { + const jobId: string = dataForJob.key; + const resultsForTime: Record = {}; + const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); + _.each(dataByTime, (dataForTime: any) => { + const time: string = dataForTime.key; + const events: object[] = _.get(dataForTime, ['events', 'buckets']); + resultsForTime[time] = _.map(events, 'key'); + }); + obj.events[jobId] = resultsForTime; + }); + + return obj; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts new file mode 100644 index 0000000000000..473477a15c2f7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getScoresByBucket( + jobIds: string[], + earliestMs: number, + latestMs: number, + interval: string | number, + maxResults: number +): Promise; +export function getTopInfluencers(): Promise; +export function getTopInfluencerValues(): Promise; +export function getOverallBucketScores( + jobIds: any, + topN: any, + earliestMs: any, + latestMs: any, + interval?: any +): Promise; +export function getInfluencerValueMaxScoreByTime(): Promise; +export function getRecordInfluencers(): Promise; +export function getRecordsForInfluencer(): Promise; +export function getRecordsForDetector(): Promise; +export function getRecords(): Promise; +export function getEventRateData( + index: string, + query: any, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string | number +): Promise; +export function getEventDistributionData(): Promise; +export function getRecordMaxScoreByTime(): Promise; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js new file mode 100644 index 0000000000000..080ba718964c4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js @@ -0,0 +1,1329 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +// Service for carrying out Elasticsearch queries to obtain data for the +// Ml Results dashboards. +import _ from 'lodash'; +// import d3 from 'd3'; + +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { escapeForElasticsearchQuery } from '../../util/string_utils'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; + +import { ml } from '../ml_api_service'; + +// Obtains the maximum bucket anomaly scores by job ID and time. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a results property, with a key for job +// which has results for the specified time range. +export function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + results: {} + }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + } + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [{ + query_string: { + query: 'result_type:bucket', + analyze_wildcard: false + } + }, { + bool: { + must: boolCriteria + } + }] + } + }, + aggs: { + jobId: { + terms: { + field: 'job_id', + size: maxResults !== undefined ? maxResults : 5, + order: { + anomalyScore: 'desc' + } + }, + aggs: { + anomalyScore: { + max: { + field: 'anomaly_score' + } + }, + byTime: { + date_histogram: { + field: 'timestamp', + interval: interval, + min_doc_count: 1, + extended_bounds: { + min: earliestMs, + max: latestMs + } + }, + aggs: { + anomalyScore: { + max: { + field: 'anomaly_score' + } + } + } + } + } + } + } + } + }) + .then((resp) => { + const dataByJobId = _.get(resp, ['aggregations', 'jobId', 'buckets'], []); + _.each(dataByJobId, (dataForJob) => { + const jobId = dataForJob.key; + + const resultsForTime = {}; + + const dataByTime = _.get(dataForJob, ['byTime', 'buckets'], []); + _.each(dataByTime, (dataForTime) => { + const value = _.get(dataForTime, ['anomalyScore', 'value']); + if (value !== undefined) { + const time = dataForTime.key; + resultsForTime[time] = _.get(dataForTime, ['anomalyScore', 'value']); + } + }); + obj.results[jobId] = resultsForTime; + }); + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s). +// Pass an empty array or ['*'] to search over all job IDs. +// An optional array of influencers may be supplied, with each object in the array having 'fieldName' +// and 'fieldValue' properties, to limit data to the supplied list of influencers. +// Returned response contains an influencers property, with a key for each of the influencer field names, +// whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. +export function getTopInfluencers( + jobIds, + earliestMs, + latestMs, + maxFieldValues = 10, + influencers = [], + influencersFilterQuery) { + return new Promise((resolve, reject) => { + const obj = { success: true, influencers: {} }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { + range: { + influencer_score: { + gt: 0 + } + } + } + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + // Add a should query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + bool: { + must: [ + { term: { influencer_field_name: influencer.fieldName } }, + { term: { influencer_field_value: influencer.fieldValue } } + ] + } + }; + }), + minimum_should_match: 1, + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:influencer', + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + aggs: { + influencerFieldNames: { + terms: { + field: 'influencer_field_name', + size: 5, + order: { + maxAnomalyScore: 'desc' + } + }, + aggs: { + maxAnomalyScore: { + max: { + field: 'influencer_score' + } + }, + influencerFieldValues: { + terms: { + field: 'influencer_field_value', + size: maxFieldValues, + order: { + maxAnomalyScore: 'desc' + } + }, + aggs: { + maxAnomalyScore: { + max: { + field: 'influencer_score' + } + }, + sumAnomalyScore: { + sum: { + field: 'influencer_score' + } + } + } + } + } + } + } + } + }) + .then((resp) => { + const fieldNameBuckets = _.get(resp, ['aggregations', 'influencerFieldNames', 'buckets'], []); + _.each(fieldNameBuckets, (nameBucket) => { + const fieldName = nameBucket.key; + const fieldValues = []; + + const fieldValueBuckets = _.get(nameBucket, ['influencerFieldValues', 'buckets'], []); + _.each(fieldValueBuckets, (valueBucket) => { + const fieldValueResult = { + influencerFieldValue: valueBucket.key, + maxAnomalyScore: valueBucket.maxAnomalyScore.value, + sumAnomalyScore: valueBucket.sumAnomalyScore.value + }; + fieldValues.push(fieldValueResult); + }); + + obj.influencers[fieldName] = fieldValues; + }); + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Obtains the top influencer field values, by maximum anomaly score, for a +// particular index, field name and job ID(s). +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a results property, which is an array of objects +// containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. +export function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { + return new Promise((resolve, reject) => { + const obj = { success: true, results: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria = [{ + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(influencerFieldName)}`, + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + aggs: { + influencerFieldValues: { + terms: { + field: 'influencer_field_value', + size: maxResults !== undefined ? maxResults : 2, + order: { + maxAnomalyScore: 'desc' + } + }, + aggs: { + maxAnomalyScore: { + max: { + field: 'influencer_score' + } + }, + sumAnomalyScore: { + sum: { + field: 'influencer_score' + } + } + } + } + } + } + }) + .then((resp) => { + const buckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []); + _.each(buckets, (bucket) => { + const result = { + influencerFieldValue: bucket.key, + maxAnomalyScore: bucket.maxAnomalyScore.value, + sumAnomalyScore: bucket.sumAnomalyScore.value }; + obj.results.push(result); + }); + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Obtains the overall bucket scores for the specified job ID(s). +// Pass ['*'] to search over all job IDs. +// Returned response contains a results property as an object of max score by time. +export function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { + return new Promise((resolve, reject) => { + const obj = { success: true, results: {} }; + + ml.overallBuckets({ + jobId: jobIds, + topN: topN, + bucketSpan: interval, + start: earliestMs, + end: latestMs + }) + .then(resp => { + const dataByTime = _.get(resp, ['overall_buckets'], []); + _.each(dataByTime, (dataForTime) => { + const value = _.get(dataForTime, ['overall_score']); + if (value !== undefined) { + obj.results[dataForTime.timestamp] = value; + } + }); + + resolve(obj); + }) + .catch(resp => { + reject(resp); + }); + }); +} + +// Obtains the maximum score by influencer_field_value and by time for the specified job ID(s) +// (pass an empty array or ['*'] to search over all job IDs), and specified influencer field +// values (pass an empty array to search over all field values). +// Returned response contains a results property with influencer field values keyed +// against max score by time. +export function getInfluencerValueMaxScoreByTime( + jobIds, + influencerFieldName, + influencerFieldValues, + earliestMs, + latestMs, + interval, + maxResults, + influencersFilterQuery) { + return new Promise((resolve, reject) => { + const obj = { success: true, results: {} }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { + range: { + influencer_score: { + gt: 0 + } + } + } + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += `job_id:${jobId}`; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + if (influencerFieldValues && influencerFieldValues.length > 0) { + let influencerFilterStr = ''; + _.each(influencerFieldValues, (value, i) => { + if (i > 0) { + influencerFilterStr += ' OR '; + } + if (value.trim().length > 0) { + influencerFilterStr += `influencer_field_value:${escapeForElasticsearchQuery(value)}`; + } else { + // Wrap whitespace influencer field values in quotes for the query_string query. + influencerFilterStr += `influencer_field_value:"${value}"`; + } + + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: influencerFilterStr + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(influencerFieldName)}`, + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + aggs: { + influencerFieldValues: { + terms: { + field: 'influencer_field_value', + size: maxResults !== undefined ? maxResults : 10, + order: { + maxAnomalyScore: 'desc' + } + }, + aggs: { + maxAnomalyScore: { + max: { + field: 'influencer_score' + } + }, + byTime: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1 + }, + aggs: { + maxAnomalyScore: { + max: { + field: 'influencer_score' + } + } + } + } + } + } + } + } + }) + .then((resp) => { + const fieldValueBuckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []); + _.each(fieldValueBuckets, (valueBucket) => { + const fieldValue = valueBucket.key; + const fieldValues = {}; + + const timeBuckets = _.get(valueBucket, ['byTime', 'buckets'], []); + _.each(timeBuckets, (timeBucket) => { + const time = timeBucket.key; + const score = timeBucket.maxAnomalyScore.value; + fieldValues[time] = score; + }); + + obj.results[fieldValue] = fieldValues; + }); + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Queries Elasticsearch to obtain record level results containing the influencers +// for the specified job(s), record score threshold, and time range. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a records property, with each record containing +// only the fields job_id, detector_index, record_score and influencers. +export function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { + return new Promise((resolve, reject) => { + const obj = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the existence of the nested influencers field, time range, + // record score, plus any specified job IDs. + const boolCriteria = [ + { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + exists: { field: 'influencers' } + } + ] + } + } + } + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { + range: { + record_score: { + gte: threshold, + } + } + } + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: maxResults !== undefined ? maxResults : 100, + rest_total_hits_as_int: true, + body: { + _source: ['job_id', 'detector_index', 'influencers', 'record_score'], + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + sort: [ + { record_score: { order: 'desc' } } + ], + } + }) + .then((resp) => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + + +// Queries Elasticsearch to obtain the record level results containing the specified influencer(s), +// for the specified job(s), time range, and record score threshold. +// influencers parameter must be an array, with each object in the array having 'fieldName' +// 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, +// so this returns record level results which have at least one of the influencers. +// Pass an empty array or ['*'] to search over all job IDs. +export function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { + return new Promise((resolve, reject) => { + const obj = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { + range: { + record_score: { + gte: threshold, + } + } + } + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr + } + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + // Add a nested query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': influencer.fieldName + } + }, + { + match: { + 'influencers.influencer_field_values': influencer.fieldValue + } + } + ] + } + } + } + }; + }), + minimum_should_match: 1, + } + }); + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: maxResults !== undefined ? maxResults : 100, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + sort: [ + { record_score: { order: 'desc' } } + ] + } + }) + .then((resp) => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + + +// Queries Elasticsearch to obtain the record level results for the specified job and detector, +// time range, record score threshold, and whether to only return results containing influencers. +// An additional, optional influencer field name and value may also be provided. +export function getRecordsForDetector( + jobId, + detectorIndex, + checkForInfluencers, + influencerFieldName, + influencerFieldValue, + threshold, + earliestMs, + latestMs, + maxResults) { + return new Promise((resolve, reject) => { + const obj = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { + term: { job_id: jobId } + }, + { + term: { detector_index: detectorIndex } + }, + { + range: { + record_score: { + gte: threshold, + } + } + } + ]; + + // Add a nested query to filter for the specified influencer field name and value. + if (influencerFieldName && influencerFieldValue) { + boolCriteria.push({ + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': influencerFieldName + } + }, + { + match: { + 'influencers.influencer_field_values': influencerFieldValue + } + } + ] + } + } + } + }); + } else { + if (checkForInfluencers === true) { + boolCriteria.push({ + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + exists: { field: 'influencers' } + } + ] + } + } + } + }); + } + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: maxResults !== undefined ? maxResults : 100, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false + } + }, + { + bool: { + must: boolCriteria + } + } + ] + } + }, + sort: [ + { record_score: { order: 'desc' } } + ], + } + }) + .then((resp) => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Queries Elasticsearch to obtain all the record level results for the specified job(s), time range, +// and record score threshold. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a records property, which is an array of the matching results. +export function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { + return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); +} + +// Queries Elasticsearch to obtain event rate data i.e. the count +// of documents over time. +// index can be a String, or String[], of index names to search. +// Extra query object can be supplied, or pass null if no additional query. +// Returned response contains a results property, which is an object +// of document counts against time (epoch millis). +export function getEventRateData( + index, + query, + timeFieldName, + earliestMs, + latestMs, + interval) { + return new Promise((resolve, reject) => { + const obj = { success: true, results: {} }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, entity fields, + // plus any additional supplied query. + const mustCriteria = [{ + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }]; + + if (query) { + mustCriteria.push(query); + } + + ml.esSearch({ + index, + rest_total_hits_as_int: true, + size: 0, + body: { + query: { + bool: { + must: mustCriteria + } + }, + _source: { + excludes: [] + }, + aggs: { + eventRate: { + date_histogram: { + field: timeFieldName, + interval: interval, + min_doc_count: 0, + extended_bounds: { + min: earliestMs, + max: latestMs, + } + } + } + } + } + }) + .then((resp) => { + const dataByTimeBucket = _.get(resp, ['aggregations', 'eventRate', 'buckets'], []); + _.each(dataByTimeBucket, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = dataForTime.doc_count; + }); + obj.total = resp.hits.total; + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Queries Elasticsearch to obtain event distribution i.e. the count +// of entities over time. +// index can be a String, or String[], of index names to search. +// Extra query object can be supplied, or pass null if no additional query. +// Returned response contains a results property, which is an object +// of document counts against time (epoch millis). +const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000; +const ENTITY_AGGREGATION_SIZE = 10; +const AGGREGATION_MIN_DOC_COUNT = 1; +const CARDINALITY_PRECISION_THRESHOLD = 100; +export function getEventDistributionData( + index, + splitField, + filterField = null, + query, + metricFunction, // ES aggregation name + metricFieldName, + timeFieldName, + earliestMs, + latestMs, + interval) { + return new Promise((resolve, reject) => { + if (splitField === undefined) { + return resolve([]); + } + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, entity fields, + // plus any additional supplied query. + const mustCriteria = []; + + mustCriteria.push({ + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }); + + if (query) { + mustCriteria.push(query); + } + + if (filterField !== null) { + mustCriteria.push({ + term: { + [filterField.fieldName]: filterField.fieldValue + } + }); + } + + const body = { + query: { + // using function_score and random_score to get a random sample of documents. + // otherwise all documents would have the same score and the sampler aggregation + // would pick the first N documents instead of a random set. + function_score: { + query: { + bool: { + must: mustCriteria + } + }, + functions: [ + { + random_score: { + // static seed to get same randomized results on every request + seed: 10, + field: '_seq_no' + } + } + ] + } + }, + size: 0, + _source: { + excludes: [] + }, + aggs: { + sample: { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE + }, + aggs: { + byTime: { + date_histogram: { + field: timeFieldName, + interval: interval, + min_doc_count: AGGREGATION_MIN_DOC_COUNT + }, + aggs: { + entities: { + terms: { + field: splitField.fieldName, + size: ENTITY_AGGREGATION_SIZE, + min_doc_count: AGGREGATION_MIN_DOC_COUNT + } + } + } + } + } + } + } + }; + + if (metricFieldName !== undefined && metricFieldName !== '') { + body.aggs.sample.aggs.byTime.aggs.entities.aggs = {}; + + const metricAgg = { + [metricFunction]: { + field: metricFieldName + } + }; + + if (metricFunction === 'percentiles') { + metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; + } + + if (metricFunction === 'cardinality') { + metricAgg[metricFunction].precision_threshold = CARDINALITY_PRECISION_THRESHOLD; + } + body.aggs.sample.aggs.byTime.aggs.entities.aggs.metric = metricAgg; + } + + ml.esSearch({ + index, + body, + rest_total_hits_as_int: true, + }) + .then((resp) => { + // Because of the sampling, results of metricFunctions which use sum or count + // can be significantly skewed. Taking into account totalHits we calculate a + // a factor to normalize results for these metricFunctions. + const totalHits = _.get(resp, ['hits', 'total'], 0); + const successfulShards = _.get(resp, ['_shards', 'successful'], 0); + + let normalizeFactor = 1; + if (totalHits > (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE)) { + normalizeFactor = totalHits / (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE); + } + + const dataByTime = _.get(resp, ['aggregations', 'sample', 'byTime', 'buckets'], []); + const data = dataByTime.reduce((d, dataForTime) => { + const date = +dataForTime.key; + const entities = _.get(dataForTime, ['entities', 'buckets'], []); + entities.forEach((entity) => { + let value = (metricFunction === 'count') ? entity.doc_count : entity.metric.value; + + if ( + metricFunction === 'count' + || metricFunction === 'cardinality' + || metricFunction === 'sum' + ) { + value = value * normalizeFactor; + } + + d.push({ + date, + entity: entity.key, + value + }); + }); + return d; + }, []); + resolve(data); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +// Queries Elasticsearch to obtain the max record score over time for the specified job, +// criteria, time range, and aggregation interval. +// criteriaFields parameter must be an array, with each object in the array having 'fieldName' +// 'fieldValue' properties. +export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + results: {} + }; + + // Build the criteria to use in the bool filter part of the request. + const mustCriteria = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }, + { term: { job_id: jobId } } + ]; + const shouldCriteria = []; + + _.each(criteriaFields, (criteria) => { + if (criteria.fieldValue.length !== 0) { + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue + } + }); + } else { + // Add special handling for blank entity field values, checking for either + // an empty string or the field not existing. + const emptyFieldCondition = { + bool: { + must: [ + { + term: { + } + } + ] + } + }; + emptyFieldCondition.bool.must[0].term[criteria.fieldName] = ''; + shouldCriteria.push(emptyFieldCondition); + shouldCriteria.push({ + bool: { + must_not: [ + { + exists: { field: criteria.fieldName } + } + ] + } + }); + } + + }); + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [{ + query_string: { + query: 'result_type:record', + analyze_wildcard: true + } + }, { + bool: { + must: mustCriteria + } + }] + } + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval: interval, + min_doc_count: 1 + }, + aggs: { + recordScore: { + max: { + field: 'record_score' + } + } + } + } + } + } + }) + .then((resp) => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = { + score: _.get(dataForTime, ['recordScore', 'value']), + }; + }); + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); +} diff --git a/x-pack/legacy/plugins/ml/public/services/table_service.js b/x-pack/legacy/plugins/ml/public/application/services/table_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/table_service.js rename to x-pack/legacy/plugins/ml/public/application/services/table_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/timefilter_refresh_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/timefilter_refresh_service.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/timefilter_refresh_service.tsx rename to x-pack/legacy/plugins/ml/public/application/services/timefilter_refresh_service.tsx diff --git a/x-pack/legacy/plugins/ml/public/services/upgrade_service.ts b/x-pack/legacy/plugins/ml/public/application/services/upgrade_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/upgrade_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/upgrade_service.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/_settings.scss b/x-pack/legacy/plugins/ml/public/application/settings/_settings.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/_settings.scss rename to x-pack/legacy/plugins/ml/public/application/settings/_settings.scss diff --git a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts new file mode 100644 index 0000000000000..bd04003c9eca4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; + +export function getSettingsBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS]; +} + +export function getCalendarManagementBreadcrumbs() { + return [ + ...getSettingsBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + href: '#/settings/calendars_list', + }, + ]; +} + +export function getCreateCalendarBreadcrumbs() { + return [ + ...getCalendarManagementBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/calendars_list/new_calendar', + }, + ]; +} + +export function getEditCalendarBreadcrumbs() { + return [ + ...getCalendarManagementBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/calendars_list/edit_calendar', + }, + ]; +} + +export function getFilterListsBreadcrumbs() { + return [ + ...getSettingsBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + href: '#/settings/filter_lists', + }, + ]; +} + +export function getCreateFilterListBreadcrumbs() { + return [ + ...getFilterListsBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/filter_lists/new', + }, + ]; +} + +export function getEditFilterListBreadcrumbs() { + return [ + ...getFilterListsBreadcrumbs(), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/filter_lists/edit', + }, + ]; +} diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/_edit.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_edit.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/_edit.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_edit.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js index fe1788db7f3fc..5754104b0e904 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import chrome from 'ui/chrome'; -import { EventsTable } from '../events_table/'; +import { EventsTable } from '../events_table'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/utils.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js index e3ba5a4851fad..153860e73829e 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js @@ -12,7 +12,7 @@ import { EuiText, EuiSpacer } from '@elastic/eui'; -import { EventsTable } from '../events_table/'; +import { EventsTable } from '../events_table'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 12c8339c52d71..feabd60d8d3a0 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -24,8 +24,8 @@ import { toastNotifications } from 'ui/notify'; import { NavigationMenu } from '../../../components/navigation_menu'; import { getCalendarSettingsData, validateCalendarId } from './utils'; -import { CalendarForm } from './calendar_form/'; -import { NewEventModal } from './new_event_modal/'; +import { CalendarForm } from './calendar_form'; +import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 2b554c2d46c1b..949e93bec76bc 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -28,7 +28,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import moment from 'moment'; -import { TIME_FORMAT } from '../events_table/'; +import { TIME_FORMAT } from '../events_table'; import { generateTempId } from '../utils'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js new file mode 100644 index 0000000000000..d97a6f62c716a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js @@ -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 { ml } from '../../../services/ml_api_service'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { i18n } from '@kbn/i18n'; + + +function getJobIds() { + return new Promise((resolve, reject) => { + ml.jobs.jobsSummary() + .then((resp) => { + resolve(resp.map((job) => job.id)); + }) + .catch((err) => { + const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithFetchingJobSummariesErrorMessage', { + defaultMessage: 'Error fetching job summaries: {err}', + values: { err } + }); + console.log(errorMessage); + reject(errorMessage); + }); + }); +} + +function getGroupIds() { + return new Promise((resolve, reject) => { + ml.jobs.groups() + .then((resp) => { + resolve(resp.map((group) => group.id)); + }) + .catch((err) => { + const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingGroupsErrorMessage', { + defaultMessage: 'Error loading groups: {err}', + values: { err } + }); + console.log(errorMessage); + reject(errorMessage); + }); + }); +} + +function getCalendars() { + return new Promise((resolve, reject) => { + ml.calendars() + .then((resp) => { + resolve(resp); + }) + .catch((err) => { + const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingCalendarsErrorMessage', { + defaultMessage: 'Error loading calendars: {err}', + values: { err } + }); + console.log(errorMessage); + reject(errorMessage); + }); + }); +} + +export function getCalendarSettingsData() { + return new Promise(async (resolve, reject) => { + try { + const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]); + + const formattedData = { + jobIds: data[0], + groupIds: data[1], + calendars: data[2] + }; + resolve(formattedData); + } catch (error) { + console.log(error); + reject(error); + } + }); +} + +export function validateCalendarId(calendarId) { + let valid = true; + + if (calendarId === '' || calendarId === undefined) { + valid = false; + } else if (isJobIdValid(calendarId) === false) { + valid = false; + } + + return valid; +} + +export function generateTempId() { + return Math.random().toString(36).substr(2, 9); +} diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/_list.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/_list.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_list.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js index b7ad2c36f3b43..ef12a6ecc1618 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js @@ -19,7 +19,7 @@ import { import { NavigationMenu } from '../../../components/navigation_menu'; import { CalendarsListHeader } from './header'; -import { CalendarsListTable } from './table/'; +import { CalendarsListTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { toastNotifications } from 'ui/notify'; import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/delete_calendars.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/delete_calendars.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/_filter_lists.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_filter_lists.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/_filter_lists.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_filter_lists.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_edit.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_edit.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_edit.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_edit.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.test.js diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js new file mode 100644 index 0000000000000..a29487672ad90 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { toastNotifications } from 'ui/notify'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { ml } from '../../../services/ml_api_service'; + +export function isValidFilterListId(id) { + // Filter List ID requires the same format as a Job ID, therefore isJobIdValid can be used + return (id !== undefined) && (id.length > 0) && isJobIdValid(id); +} + + +// Saves a filter list, running an update if the supplied loadedFilterList, holding the +// original filter list to which edits are being applied, is defined with a filter_id property. +export function saveFilterList(filterId, description, items, loadedFilterList) { + return new Promise((resolve, reject) => { + if (loadedFilterList === undefined || loadedFilterList.filter_id === undefined) { + // Create a new filter. + addFilterList(filterId, + description, + items + ) + .then((newFilter) => { + resolve(newFilter); + }) + .catch((error) => { + reject(error); + }); + } else { + // Edit to existing filter. + updateFilterList( + loadedFilterList, + description, + items) + .then((updatedFilter) => { + resolve(updatedFilter); + }) + .catch((error) => { + reject(error); + }); + + } + }); +} + +export function addFilterList(filterId, description, items) { + const filterWithIdExistsErrorMessage = i18n.translate('xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage', { + defaultMessage: 'A filter with id {filterId} already exists', + values: { + filterId, + }, + }); + + return new Promise((resolve, reject) => { + + // First check the filterId isn't already in use by loading the current list of filters. + ml.filters.filtersStats() + .then((filterLists) => { + const savedFilterIds = filterLists.map(filterList => filterList.filter_id); + if (savedFilterIds.indexOf(filterId) === -1) { + // Save the new filter. + ml.filters.addFilter( + filterId, + description, + items + ) + .then((newFilter) => { + resolve(newFilter); + }) + .catch((error) => { + reject(error); + }); + } else { + toastNotifications.addDanger(filterWithIdExistsErrorMessage); + reject(new Error(filterWithIdExistsErrorMessage)); + } + }) + .catch((error) => { + reject(error); + }); + + }); + +} + +export function updateFilterList(loadedFilterList, description, items) { + + return new Promise((resolve, reject) => { + + // Get items added and removed from loaded filter. + const loadedItems = loadedFilterList.items; + const addItems = items.filter(item => (loadedItems.includes(item) === false)); + const removeItems = loadedItems.filter(item => (items.includes(item) === false)); + + ml.filters.updateFilter( + loadedFilterList.filter_id, + description, + addItems, + removeItems + ) + .then((updatedFilter) => { + resolve(updatedFilter); + }) + .catch((error) => { + reject(error); + }); + }); +} diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.test.js b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/settings.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/settings_directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings_directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings_directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/settings_directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_index.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer_annotations.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer_annotations.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js new file mode 100644 index 0000000000000..2aa4c845b125d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; +import { i18n } from '@kbn/i18n'; + + +export function getSingleMetricViewerBreadcrumbs() { + // Whilst top level nav menu with tabs remains, + // use root ML breadcrumb. + return [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { + defaultMessage: 'Single Metric Viewer' + }), + href: '' + } + + ]; +} + diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecast_progress.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecast_progress.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecast_progress.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecast_progress.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 00812d56ade4a..26fffb5e481ee 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -24,15 +24,15 @@ import { timefilter } from 'ui/timefilter'; // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../common/constants/states'; -import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; -import { isJobVersionGte } from '../../../../common/util/job_utils'; -import { parseInterval } from '../../../../common/util/parse_interval'; +import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; +import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; +import { isJobVersionGte } from '../../../../../common/util/job_utils'; +import { parseInterval } from '../../../../../common/util/parse_interval'; import { Modal } from './modal'; import { PROGRESS_STATES } from './progress_states'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { mlForecastService } from 'plugins/ml/services/forecast_service'; +import { ml } from '../../../services/ml_api_service'; +import { mlJobService } from '../../../services/job_service'; +import { mlForecastService } from '../../../services/forecast_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasts_list.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasts_list.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/modal.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index dcde78e5e0b32..47051eecf9d04 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -25,7 +25,7 @@ import { } from '@elastic/eui'; -import { MessageCallOut } from 'plugins/ml/components/message_call_out'; +import { MessageCallOut } from '../../../components/message_call_out'; import { ForecastsList } from './forecasts_list'; import { RunControls } from './run_controls'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_icon.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_icon.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_icon.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_icon.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_states.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_states.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_states.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_states.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js similarity index 96% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index 0b2a35fb3e39a..fef992719749e 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -28,11 +28,11 @@ import { // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { JOB_STATE } from '../../../../common/constants/states'; +import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { checkPermission, createPermissionFailureMessage } from 'plugins/ml/privilege/check_privilege'; +import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { checkPermission, createPermissionFailureMessage } from '../../../privilege/check_privilege'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts similarity index 79% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 62ba8cfbe7d34..1f49ec1826422 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -6,8 +6,8 @@ import d3 from 'd3'; -import { Annotation } from '../../../../common/types/annotations'; -import { MlJob } from '../../../../common/types/jobs'; +import { Annotation } from '../../../../../common/types/annotations'; +import { MlJob } from '../../../../../common/types/jobs'; interface Props { selectedJob: MlJob; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js similarity index 99% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 81852f025fb1f..eb4dfae3f5ff3 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -24,7 +24,7 @@ import chrome from 'ui/chrome'; import { getSeverityWithLow, getMultiBucketImpactLabel, -} from '../../../../common/util/anomaly_utils'; +} from '../../../../../common/util/anomaly_utils'; import { annotation$ } from '../../../services/annotations_service'; import { injectObservablesAsProps } from '../../../util/observable_utils'; import { formatValue } from '../../../formatters/format_value'; @@ -1081,10 +1081,6 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo const that = this; function brushed() { - if (that.props.skipRefresh) { - return; - } - const isEmpty = brush.empty(); const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index e668b39edd784..925107eb5f573 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -7,9 +7,9 @@ import d3 from 'd3'; import moment from 'moment'; -import { ANNOTATION_TYPE } from '../../../../common/constants/annotations'; -import { Annotation, Annotations } from '../../../../common/types/annotations'; -import { Dictionary } from '../../../../common/types/common'; +import { ANNOTATION_TYPE } from '../../../../../common/constants/annotations'; +import { Annotation, Annotations } from '../../../../../common/types/annotations'; +import { Dictionary } from '../../../../../common/types/common'; // @ts-ignore import { mlChartTooltipService } from '../../../components/chart_tooltip/chart_tooltip_service'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js new file mode 100644 index 0000000000000..5aa6cfe8835ad --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './timeseriesexplorer_directive'; +import './timeseriesexplorer_route'; +import './timeseries_search_service'; +import '../components/job_selector'; +import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts new file mode 100644 index 0000000000000..65bcc9d355fd6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; + +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ml } from '../services/ml_api_service'; +import { isModelPlotEnabled } from '../../../common/util/job_utils'; +// @ts-ignore +import { buildConfigFromDetector } from '../util/chart_config_builder'; +import { mlResultsService } from '../services/results_service'; +import { ModelPlotOutput } from '../services/results_service/result_service_rx'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +function getMetricData( + job: Job, + detectorIndex: number, + entityFields: object[], + earliestMs: number, + latestMs: number, + interval: string +): Observable { + if (isModelPlotEnabled(job, detectorIndex, entityFields)) { + // Extract the partition, by, over fields on which to filter. + const criteriaFields = []; + const detector = job.analysis_config.detectors[detectorIndex]; + if (_.has(detector, 'partition_field_name')) { + const partitionEntity: any = _.find(entityFields, { + fieldName: detector.partition_field_name, + }); + if (partitionEntity !== undefined) { + criteriaFields.push( + { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue } + ); + } + } + + if (_.has(detector, 'over_field_name')) { + const overEntity: any = _.find(entityFields, { fieldName: detector.over_field_name }); + if (overEntity !== undefined) { + criteriaFields.push( + { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue } + ); + } + } + + if (_.has(detector, 'by_field_name')) { + const byEntity: any = _.find(entityFields, { fieldName: detector.by_field_name }); + if (byEntity !== undefined) { + criteriaFields.push( + { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue } + ); + } + } + + return mlResultsService.getModelPlotOutput( + job.job_id, + detectorIndex, + criteriaFields, + earliestMs, + latestMs, + interval + ); + } else { + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; + + const chartConfig = buildConfigFromDetector(job, detectorIndex); + + return mlResultsService + .getMetricData( + chartConfig.datafeedConfig.indices, + entityFields, + chartConfig.datafeedConfig.query, + chartConfig.metricFunction, + chartConfig.metricFieldName, + chartConfig.timeField, + earliestMs, + latestMs, + interval + ) + .pipe( + map(resp => { + _.each(resp.results, (value, time) => { + // @ts-ignore + obj.results[time] = { + actual: value, + }; + }); + return obj; + }) + ); + } +} + +// Builds chart detail information (charting function description and entity counts) used +// in the title area of the time series chart. +// Queries Elasticsearch if necessary to obtain the distinct count of entities +// for which data is being plotted. +function getChartDetails( + job: Job, + detectorIndex: number, + entityFields: any[], + earliestMs: number, + latestMs: number +) { + return new Promise((resolve, reject) => { + const obj: any = { + success: true, + results: { functionLabel: '', entityData: { entities: [] } }, + }; + + const chartConfig = buildConfigFromDetector(job, detectorIndex); + let functionLabel = chartConfig.metricFunction; + if (chartConfig.metricFieldName !== undefined) { + functionLabel += ' '; + functionLabel += chartConfig.metricFieldName; + } + obj.results.functionLabel = functionLabel; + + const blankEntityFields = _.filter(entityFields, entity => { + return entity.fieldValue.length === 0; + }); + + // Look to see if any of the entity fields have defined values + // (i.e. blank input), and if so obtain the cardinality. + if (blankEntityFields.length === 0) { + obj.results.entityData.count = 1; + obj.results.entityData.entities = entityFields; + resolve(obj); + } else { + const entityFieldNames = _.map(blankEntityFields, 'fieldName'); + ml.getCardinalityOfFields({ + index: chartConfig.datafeedConfig.indices, + fieldNames: entityFieldNames, + query: chartConfig.datafeedConfig.query, + timeFieldName: chartConfig.timeField, + earliestMs, + latestMs, + }) + .then((results: any) => { + _.each(blankEntityFields, field => { + // results will not contain keys for non-aggregatable fields, + // so store as 0 to indicate over all field values. + obj.results.entityData.entities.push({ + fieldName: field.fieldName, + cardinality: _.get(results, field.fieldName, 0), + }); + }); + + resolve(obj); + }) + .catch((resp: any) => { + reject(resp); + }); + } + }); +} + +export const mlTimeSeriesSearchService = { + getMetricData, + getChartDetails, +}; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js similarity index 85% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 1dec12a396578..02e29c1117ffc 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,9 +8,10 @@ * React component for rendering Single Metric Viewer. */ -import { chain, difference, each, find, filter, first, get, has, isEqual, without } from 'lodash'; +import { chain, difference, each, find, first, get, has, isEqual, without } from 'lodash'; import moment from 'moment-timezone'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription, forkJoin } from 'rxjs'; +import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import PropTypes from 'prop-types'; import React, { createRef, Fragment } from 'react'; @@ -32,17 +33,17 @@ import { import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; -import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; +import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; -import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../common/constants/search'; -import { parseInterval } from '../../common/util/parse_interval'; +import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { parseInterval } from '../../../common/util/parse_interval'; import { isModelPlotEnabled, isSourceDataChartableForDetector, isTimeSeriesViewJob, isTimeSeriesViewDetector, mlFunctionToESAggregation, -} from '../../common/util/job_utils'; +} from '../../../common/util/job_utils'; import { ChartTooltip } from '../components/chart_tooltip'; import { jobSelectServiceFactory, setGlobalState, getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; @@ -78,11 +79,10 @@ import { calculateInitialFocusRange, createTimeSeriesJobData, getAutoZoomDuration, - getFocusData, processForecastResults, processMetricPlotResults, processRecordScoreResults, -} from './timeseriesexplorer_utils'; + getFocusData } from './timeseriesexplorer_utils'; const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); @@ -179,6 +179,11 @@ export class TimeSeriesExplorer extends React.Component { }); } + /** + * Subject for listening brush time range selection. + */ + contextChart$ = new Subject(); + detectorIndexChangeHandler = (e) => { const id = e.target.value; if (id !== undefined) { @@ -252,109 +257,85 @@ export class TimeSeriesExplorer extends React.Component { } contextChartSelectedInitCallDone = false; - contextChartSelected = (selection) => { - const { appStateHandler } = this.props; + /** + * Gets default range from component state. + */ + getDefaultRangeFromState() { const { autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, - focusChartData, - jobs, - selectedJob, - zoomFromFocusLoaded, - zoomToFocusLoaded, } = this.state; - - if ((contextChartData === undefined || contextChartData.length === 0) && - (contextForecastData === undefined || contextForecastData.length === 0)) { - return; - } - - const stateUpdate = {}; - - const defaultRange = calculateDefaultFocusRange( + return calculateDefaultFocusRange( autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, ); + } - if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && - (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { - const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; - appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); - } else { - appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); - } + getFocusAggregationInterval(selection) { + const { + jobs, + selectedJob, + } = this.state; - this.setState({ - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - if ( - (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || - (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || - (zoomToFocusLoaded.getTime() !== selection.to.getTime()) - ) { - this.contextChartSelectedInitCallDone = true; + return calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + } - // Calculate the aggregation interval for the focus chart. - const bounds = { min: moment(selection.from), max: moment(selection.to) }; - const focusAggregationInterval = calculateAggregationInterval( - bounds, - CHARTS_POINT_TARGET, - jobs, - selectedJob, - ); - stateUpdate.focusAggregationInterval = focusAggregationInterval; + /** + * Gets focus data for the current component state/ + */ + getFocusData(selection) { + const { + detectorId, + entities, + modelPlotEnabled, + selectedJob, + } = this.state; - // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. - // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected - // to some extent with all detector functions if not searching complete buckets. - const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + const { appStateHandler } = this.props; - const { - detectorId, - entities, - modelPlotEnabled, - } = this.state; - - this.setState({ - loading: true, - fullRefresh: false, - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - getFocusData( - this._criteriaFields, - +detectorId, - focusAggregationInterval, - appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), - modelPlotEnabled, - filter(entities, entity => entity.fieldValue.length > 0), - searchBounds, - selectedJob, - TIME_FIELD_NAME, - ).then((refreshFocusData) => { - // All the data is ready now for a state update. - this.setState({ - ...stateUpdate, - ...refreshFocusData, - loading: false, - showModelBoundsCheckbox: (modelPlotEnabled === true) && (refreshFocusData.focusChartData.length > 0), - zoomFromFocusLoaded: selection.from, - zoomToFocusLoaded: selection.to, - }); - }); + const focusAggregationInterval = this.getFocusAggregationInterval(selection); - // Load the data for the anomalies table. - this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()); - } + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval( + bounds, + focusAggregationInterval, + false + ); + + return getFocusData( + this._criteriaFields, + +detectorId, + focusAggregationInterval, + appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), + modelPlotEnabled, + entities.filter(entity => entity.fieldValue.length > 0), + searchBounds, + selectedJob, + TIME_FIELD_NAME + ); + } + + contextChartSelected = (selection) => { + this.contextChart$.next(selection); } entityFieldValueChanged = (entity, fieldValue) => { @@ -380,7 +361,7 @@ export class TimeSeriesExplorer extends React.Component { const { dateFormatTz } = this.props; const { selectedJob } = this.state; - ml.results.getAnomaliesTableData( + return ml.results.getAnomaliesTableData( [selectedJob.job_id], this._criteriaFields, [], @@ -390,43 +371,43 @@ export class TimeSeriesExplorer extends React.Component { latestMs, dateFormatTz, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - const anomalies = resp.anomalies; - const detectorsByJob = mlJobService.detectorsByJob; - anomalies.forEach((anomaly) => { - // Add a detector property to each anomaly. - // Default to functionDescription if no description available. - // TODO - when job_service is moved server_side, move this to server endpoint. - const jobId = anomaly.jobId; - const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); - anomaly.detector = get(detector, - ['detector_description'], - anomaly.source.function_description); - - // For detectors with rules, add a property with the rule count. - const customRules = detector.custom_rules; - if (customRules !== undefined) { - anomaly.rulesLength = customRules.length; - } + ).pipe( + map(resp => { + const anomalies = resp.anomalies; + const detectorsByJob = mlJobService.detectorsByJob; + anomalies.forEach((anomaly) => { + // Add a detector property to each anomaly. + // Default to functionDescription if no description available. + // TODO - when job_service is moved server_side, move this to server endpoint. + const jobId = anomaly.jobId; + const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); + anomaly.detector = get(detector, + ['detector_description'], + anomaly.source.function_description); + + // For detectors with rules, add a property with the rule count. + const customRules = detector.custom_rules; + if (customRules !== undefined) { + anomaly.rulesLength = customRules.length; + } - // Add properties used for building the links menu. - // TODO - when job_service is moved server_side, move this to server endpoint. - if (has(mlJobService.customUrlsByJob, jobId)) { - anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; - } - }); + // Add properties used for building the links menu. + // TODO - when job_service is moved server_side, move this to server endpoint. + if (has(mlJobService.customUrlsByJob, jobId)) { + anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; + } + }); - this.setState({ - tableData: { - anomalies, - interval: resp.interval, - examplesByJobId: resp.examplesByJobId, - showViewSeriesLink: false - } - }); - }).catch((resp) => { - console.log('Time series explorer - error loading data for anomalies table:', resp); - }); + return { + tableData: { + anomalies, + interval: resp.interval, + examplesByJobId: resp.examplesByJobId, + showViewSeriesLink: false + } + }; + }) + ); } loadEntityValues = (callback = () => {}) => { @@ -445,6 +426,7 @@ export class TimeSeriesExplorer extends React.Component { bounds.min.valueOf(), bounds.max.valueOf(), ANOMALIES_TABLE_DEFAULT_QUERY_SIZE) + .toPromise() .then((resp) => { if (resp.records && resp.records.length > 0) { const firstRec = resp.records[0]; @@ -604,7 +586,7 @@ export class TimeSeriesExplorer extends React.Component { } }; - const nonBlankEntities = filter(currentEntities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = currentEntities.filter((entity) => { return entity.fieldValue.length > 0; }); if (modelPlotEnabled === false && isSourceDataChartableForDetector(selectedJob, detectorIndex) === false && @@ -646,7 +628,7 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression - ).then((resp) => { + ).toPromise().then((resp) => { const fullRangeChartData = processMetricPlotResults(resp.results, modelPlotEnabled); stateUpdate.contextChartData = fullRangeChartData; finish(counter); @@ -702,7 +684,8 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression, - aggType) + aggType + ).toPromise() .then((resp) => { stateUpdate.contextForecastData = processForecastResults(resp.results); finish(counter); @@ -762,7 +745,7 @@ export class TimeSeriesExplorer extends React.Component { */ updateCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. - const nonBlankEntities = filter(entities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0); this._criteriaFields = [ { fieldName: 'detector_index', @@ -868,7 +851,8 @@ export class TimeSeriesExplorer extends React.Component { const tableControlsListener = () => { const { zoomFrom, zoomTo } = this.state; if (zoomFrom !== undefined && zoomTo !== undefined) { - this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()); + this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()).subscribe(res => + this.setState(res)); } }; @@ -978,6 +962,97 @@ export class TimeSeriesExplorer extends React.Component { this.resizeHandler(); }); this.resizeHandler(); + + // Listen for context chart updates. + this.subscriptions.add(this.contextChart$ + .pipe( + tap(selection => { + this.setState({ + zoomFrom: selection.from, + zoomTo: selection.to, + }); + }), + debounceTime(500), + tap((selection) => { + const { + contextChartData, + contextForecastData, + focusChartData, + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.state; + + if ((contextChartData === undefined || contextChartData.length === 0) && + (contextForecastData === undefined || contextForecastData.length === 0)) { + return; + } + + const defaultRange = this.getDefaultRangeFromState(); + + if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && + (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { + const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; + appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); + } else { + appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); + } + + if ( + (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || + (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || + (zoomToFocusLoaded.getTime() !== selection.to.getTime()) + ) { + this.contextChartSelectedInitCallDone = true; + + this.setState({ + loading: true, + fullRefresh: false, + }); + } + }), + switchMap(selection => { + const { + jobs, + selectedJob + } = this.state; + + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + const focusAggregationInterval = calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + return forkJoin([ + this.getFocusData(selection), + // Load the data for the anomalies table. + this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()) + ]); + }), + withLatestFrom(this.contextChart$) + ) + .subscribe(([[refreshFocusData, tableData], selection]) => { + const { + modelPlotEnabled, + } = this.state; + + // All the data is ready now for a state update. + this.setState({ + focusAggregationInterval: this.getFocusAggregationInterval({ from: selection.from, to: selection.to }), + loading: false, + showModelBoundsCheckbox: modelPlotEnabled && (refreshFocusData.focusChartData.length > 0), + zoomFromFocusLoaded: selection.from, + zoomToFocusLoaded: selection.to, + ...refreshFocusData, + ...tableData + }); + })); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts new file mode 100644 index 0000000000000..29a5facf64c0f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Contains values for ML time series explorer. + */ + +export const APP_STATE_ACTION = { + CLEAR: 'CLEAR', + GET_DETECTOR_INDEX: 'GET_DETECTOR_INDEX', + SET_DETECTOR_INDEX: 'SET_DETECTOR_INDEX', + GET_ENTITIES: 'GET_ENTITIES', + SET_ENTITIES: 'SET_ENTITIES', + GET_FORECAST_ID: 'GET_FORECAST_ID', + SET_FORECAST_ID: 'SET_FORECAST_ID', + GET_ZOOM: 'GET_ZOOM', + SET_ZOOM: 'SET_ZOOM', + UNSET_ZOOM: 'UNSET_ZOOM', +}; + +export const CHARTS_POINT_TARGET = 500; + +// Max number of scheduled events displayed per bucket. +export const MAX_SCHEDULED_EVENTS = 10; + +export const TIME_FIELD_NAME = 'timestamp'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_directive.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_route.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_route.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts new file mode 100644 index 0000000000000..03fe718de9bed --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import chrome from 'ui/chrome'; +import { ml } from '../../services/ml_api_service'; +import { + ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, +} from '../../../../common/constants/search'; +import { mlTimeSeriesSearchService } from '../timeseries_search_service'; +import { mlResultsService, CriteriaField } from '../../services/results_service'; +import { Job } from '../../jobs/new_job/common/job_creator/configs'; +import { MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME } from '../timeseriesexplorer_constants'; +import { + processDataForFocusAnomalies, + processForecastResults, + processMetricPlotResults, + processScheduledEventsForChart, +} from './timeseriesexplorer_utils'; +import { mlForecastService } from '../../services/forecast_service'; +import { mlFunctionToESAggregation } from '../../../../common/util/job_utils'; +import { Annotation } from '../../../../common/types/annotations'; + +const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); + +export interface Interval { + asMilliseconds: () => number; + expression: string; +} + +export interface FocusData { + focusChartData: any; + anomalyRecords: any; + scheduledEvents: any; + showForecastCheckbox?: any; + focusAnnotationData?: any; + focusForecastData?: any; +} + +export function getFocusData( + criteriaFields: CriteriaField[], + detectorIndex: number, + focusAggregationInterval: Interval, + forecastId: string, + modelPlotEnabled: boolean, + nonBlankEntities: any[], + searchBounds: any, + selectedJob: Job +): Observable { + return forkJoin([ + // Query 1 - load metric data across selected time range. + mlTimeSeriesSearchService.getMetricData( + selectedJob, + detectorIndex, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression + ), + // Query 2 - load all the records across selected time range for the chart anomaly markers. + mlResultsService.getRecordsForCriteria( + [selectedJob.job_id], + criteriaFields, + 0, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE + ), + // Query 3 - load any scheduled events for the selected job. + mlResultsService.getScheduledEventsByBucket( + [selectedJob.job_id], + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + 1, + MAX_SCHEDULED_EVENTS + ), + // Query 4 - load any annotations for the selected job. + mlAnnotationsEnabled + ? ml.annotations + .getAnnotations({ + jobIds: [selectedJob.job_id], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .pipe( + catchError(() => { + // silent fail + return of({ annotations: {} as Record }); + }) + ) + : of(null), + // Plus query for forecast data if there is a forecastId stored in the appState. + forecastId !== undefined + ? (() => { + let aggType; + const detector = selectedJob.analysis_config.detectors[detectorIndex]; + const esAgg = mlFunctionToESAggregation(detector.function); + if (!modelPlotEnabled && (esAgg === 'sum' || esAgg === 'count')) { + aggType = { avg: 'sum', max: 'sum', min: 'sum' }; + } + return mlForecastService.getForecastData( + selectedJob, + detectorIndex, + forecastId, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + aggType + ); + })() + : of(null), + ]).pipe( + map(([metricData, recordsForCriteria, scheduledEventsByBucket, annotations, forecastData]) => { + // Sort in descending time order before storing in scope. + const anomalyRecords = recordsForCriteria.records + .sort((a, b) => a[TIME_FIELD_NAME] - b[TIME_FIELD_NAME]) + .reverse(); + + const scheduledEvents = scheduledEventsByBucket.events[selectedJob.job_id]; + + let focusChartData = processMetricPlotResults(metricData.results, modelPlotEnabled); + // Tell the results container directives to render the focus chart. + focusChartData = processDataForFocusAnomalies( + focusChartData, + anomalyRecords, + focusAggregationInterval, + modelPlotEnabled + ); + focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents); + + const refreshFocusData: FocusData = { + scheduledEvents, + anomalyRecords, + focusChartData, + }; + + if (annotations) { + refreshFocusData.focusAnnotationData = (annotations.annotations[selectedJob.job_id] ?? []) + .sort((a, b) => { + return a.timestamp - b.timestamp; + }) + .map((d, i) => { + d.key = String.fromCharCode(65 + i); + return d; + }); + } + + if (forecastData) { + refreshFocusData.focusForecastData = processForecastResults(forecastData.results); + refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0; + } + + return refreshFocusData; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts new file mode 100644 index 0000000000000..578dbdf1277a0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getFocusData } from './get_focus_data'; +export * from './timeseriesexplorer_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts new file mode 100644 index 0000000000000..1528ac887ad76 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function createTimeSeriesJobData(jobs: any): any; + +export function processMetricPlotResults(metricPlotData: any, modelPlotEnabled: any): any; + +export function processForecastResults(forecastData: any): any; + +export function processRecordScoreResults(scoreData: any): any; + +export function processDataForFocusAnomalies( + chartData: any, + anomalyRecords: any, + aggregationInterval: any, + modelPlotEnabled: any +): any; + +export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any; + +export function findNearestChartPointToTime(chartData: any, time: any): any; + +export function findChartPointForAnomalyTime( + chartData: any, + anomalyTime: any, + aggregationInterval: any +): any; + +export function calculateAggregationInterval( + bounds: any, + bucketsTarget: any, + jobs: any, + selectedJob: any +): any; + +export function calculateDefaultFocusRange( + autoZoomDuration: any, + contextAggregationInterval: any, + contextChartData: any, + contextForecastData: any +): any; + +export function calculateInitialFocusRange( + zoomState: any, + contextAggregationInterval: any, + timefilter: any +): any; + +export function getAutoZoomDuration(jobs: any, selectedJob: any): any; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js new file mode 100644 index 0000000000000..b9c9ed87ddbc7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -0,0 +1,412 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +/* + * Contains a number of utility functions used for processing + * the data for exploring a time series in the Single Metric + * Viewer dashboard. + */ + +import _ from 'lodash'; +import moment from 'moment-timezone'; + +import { + isTimeSeriesViewJob, +} from '../../../../common/util/job_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; + +import { TimeBuckets, getBoundsRoundedToInterval } from '../../util/time_buckets'; + +import { + CHARTS_POINT_TARGET, + TIME_FIELD_NAME, +} from '../timeseriesexplorer_constants'; + +// create new job objects based on standard job config objects +// new job objects just contain job id, bucket span in seconds and a selected flag. +// only time series view jobs are allowed +export function createTimeSeriesJobData(jobs) { + const singleTimeSeriesJobs = jobs.filter(isTimeSeriesViewJob); + return singleTimeSeriesJobs.map(job => { + const bucketSpan = parseInterval(job.analysis_config.bucket_span); + return { + id: job.job_id, + selected: false, + bucketSpanSeconds: bucketSpan.asSeconds() + }; + }); +} + +// Return dataset in format used by the single metric chart. +// i.e. array of Objects with keys date (JavaScript date) and value, +// plus lower and upper keys if model plot is enabled for the series. +export function processMetricPlotResults(metricPlotData, modelPlotEnabled) { + const metricPlotChartData = []; + if (modelPlotEnabled === true) { + _.each(metricPlotData, (dataForTime, time) => { + metricPlotChartData.push({ + date: new Date(+time), + lower: dataForTime.modelLower, + value: dataForTime.actual, + upper: dataForTime.modelUpper + }); + }); + } else { + _.each(metricPlotData, (dataForTime, time) => { + metricPlotChartData.push({ + date: new Date(+time), + value: dataForTime.actual + }); + }); + } + + return metricPlotChartData; +} + +// Returns forecast dataset in format used by the single metric chart. +// i.e. array of Objects with keys date (JavaScript date), isForecast, +// value, lower and upper keys. +export function processForecastResults(forecastData) { + const forecastPlotChartData = []; + _.each(forecastData, (dataForTime, time) => { + forecastPlotChartData.push({ + date: new Date(+time), + isForecast: true, + lower: dataForTime.forecastLower, + value: dataForTime.prediction, + upper: dataForTime.forecastUpper + }); + }); + + return forecastPlotChartData; +} + +// Return dataset in format used by the swimlane. +// i.e. array of Objects with keys date (JavaScript date) and score. +export function processRecordScoreResults(scoreData) { + const bucketScoreData = []; + _.each(scoreData, (dataForTime, time) => { + bucketScoreData.push( + { + date: new Date(+time), + score: dataForTime.score, + }); + }); + + return bucketScoreData; +} + +// Uses data from the list of anomaly records to add anomalyScore, +// function, actual and typical properties, plus causes and multi-bucket +// info if applicable, to the chartData entries for anomalous buckets. +export function processDataForFocusAnomalies( + chartData, + anomalyRecords, + aggregationInterval, + modelPlotEnabled) { + + const timesToAddPointsFor = []; + + // Iterate through the anomaly records making sure we have chart points for each anomaly. + const intervalMs = aggregationInterval.asMilliseconds(); + let lastChartDataPointTime = undefined; + if (chartData !== undefined && chartData.length > 0) { + lastChartDataPointTime = chartData[chartData.length - 1].date.getTime(); + } + anomalyRecords.forEach((record) => { + const recordTime = record[TIME_FIELD_NAME]; + const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); + if (chartPoint === undefined) { + const timeToAdd = (Math.floor(recordTime / intervalMs)) * intervalMs; + if (timesToAddPointsFor.indexOf(timeToAdd) === -1 && timeToAdd !== lastChartDataPointTime) { + timesToAddPointsFor.push(timeToAdd); + } + } + }); + + timesToAddPointsFor.sort((a, b) => a - b); + + timesToAddPointsFor.forEach((time) => { + const pointToAdd = { + date: new Date(time), + value: null + }; + + if (modelPlotEnabled === true) { + pointToAdd.upper = null; + pointToAdd.lower = null; + } + chartData.push(pointToAdd); + }); + + // Iterate through the anomaly records adding the + // various properties required for display. + anomalyRecords.forEach((record) => { + + // Look for a chart point with the same time as the record. + // If none found, find closest time in chartData set. + const recordTime = record[TIME_FIELD_NAME]; + const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); + if (chartPoint !== undefined) { + // If chart aggregation interval > bucket span, there may be more than + // one anomaly record in the interval, so use the properties from + // the record with the highest anomalyScore. + const recordScore = record.record_score; + const pointScore = chartPoint.anomalyScore; + if (pointScore === undefined || pointScore < recordScore) { + chartPoint.anomalyScore = recordScore; + chartPoint.function = record.function; + + if (_.has(record, 'actual')) { + chartPoint.actual = record.actual; + chartPoint.typical = record.typical; + } else { + const causes = _.get(record, 'causes', []); + if (causes.length > 0) { + chartPoint.byFieldName = record.by_field_name; + chartPoint.numberOfCauses = causes.length; + if (causes.length === 1) { + // If only a single cause, copy actual and typical values to the top level. + const cause = _.first(record.causes); + chartPoint.actual = cause.actual; + chartPoint.typical = cause.typical; + } + } + } + + if (_.has(record, 'multi_bucket_impact')) { + chartPoint.multiBucketImpact = record.multi_bucket_impact; + } + } + } + + }); + + return chartData; +} + +// Adds a scheduledEvents property to any points in the chart data set +// which correspond to times of scheduled events for the job. +export function processScheduledEventsForChart(chartData, scheduledEvents) { + if (scheduledEvents !== undefined) { + _.each(scheduledEvents, (events, time) => { + const chartPoint = findNearestChartPointToTime(chartData, time); + if (chartPoint !== undefined) { + // Note if the scheduled event coincides with an absence of the underlying metric data, + // we don't worry about plotting the event. + chartPoint.scheduledEvents = events; + } + }); + } + + return chartData; +} + +// Finds the chart point which is closest in time to the specified time. +export function findNearestChartPointToTime(chartData, time) { + let chartPoint; + if(chartData === undefined) { + return chartPoint; + } + + for (let i = 0; i < chartData.length; i++) { + if (chartData[i].date.getTime() === time) { + chartPoint = chartData[i]; + break; + } + } + + if (chartPoint === undefined) { + // Find nearest point in time. + // loop through line items until the date is greater than bucketTime + // grab the current and previous items and compare the time differences + let foundItem; + for (let i = 0; i < chartData.length; i++) { + const itemTime = chartData[i].date.getTime(); + if (itemTime > time) { + const item = chartData[i]; + const previousItem = chartData[i - 1]; + + const diff1 = Math.abs(time - previousItem.date.getTime()); + const diff2 = Math.abs(time - itemTime); + + // foundItem should be the item with a date closest to bucketTime + if (previousItem === undefined || diff1 > diff2) { + foundItem = item; + } else { + foundItem = previousItem; + } + + break; + } + } + + chartPoint = foundItem; + } + + return chartPoint; +} + +// Finds the chart point which corresponds to an anomaly with the +// specified time. +export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregationInterval) { + let chartPoint; + if(chartData === undefined) { + return chartPoint; + } + + for (let i = 0; i < chartData.length; i++) { + if (chartData[i].date.getTime() === anomalyTime) { + chartPoint = chartData[i]; + break; + } + } + + if (chartPoint === undefined) { + // Find the time of the point which falls immediately before the + // time of the anomaly. This is the start of the chart 'bucket' + // which contains the anomalous bucket. + let foundItem; + const intervalMs = aggregationInterval.asMilliseconds(); + for (let i = 0; i < chartData.length; i++) { + const itemTime = chartData[i].date.getTime(); + if (anomalyTime - itemTime < intervalMs) { + foundItem = chartData[i]; + break; + } + } + + chartPoint = foundItem; + } + + return chartPoint; +} + +export function calculateAggregationInterval( + bounds, + bucketsTarget, + jobs, + selectedJob, +) { + // Aggregation interval used in queries should be a function of the time span of the chart + // and the bucket span of the selected job(s). + const barTarget = (bucketsTarget !== undefined ? bucketsTarget : 100); + // Use a maxBars of 10% greater than the target. + const maxBars = Math.floor(1.1 * barTarget); + const buckets = new TimeBuckets(); + buckets.setInterval('auto'); + buckets.setBounds(bounds); + buckets.setBarTarget(Math.floor(barTarget)); + buckets.setMaxBars(maxBars); + + // Ensure the aggregation interval is always a multiple of the bucket span to avoid strange + // behaviour such as adjacent chart buckets holding different numbers of job results. + const bucketSpanSeconds = _.find(jobs, { 'id': selectedJob.job_id }).bucketSpanSeconds; + let aggInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); + + // Set the interval back to the job bucket span if the auto interval is smaller. + const secs = aggInterval.asSeconds(); + if (secs < bucketSpanSeconds) { + buckets.setInterval(bucketSpanSeconds + 's'); + aggInterval = buckets.getInterval(); + } + + return aggInterval; +} + +export function calculateDefaultFocusRange( + autoZoomDuration, + contextAggregationInterval, + contextChartData, + contextForecastData, +) { + const isForecastData = contextForecastData !== undefined && contextForecastData.length > 0; + + const combinedData = (isForecastData === false) ? + contextChartData : contextChartData.concat(contextForecastData); + const earliestDataDate = _.first(combinedData).date; + const latestDataDate = _.last(combinedData).date; + + let rangeEarliestMs; + let rangeLatestMs; + + if (isForecastData === true) { + // Return a range centred on the start of the forecast range, depending + // on the time range of the forecast and data. + const earliestForecastDataDate = _.first(contextForecastData).date; + const latestForecastDataDate = _.last(contextForecastData).date; + + rangeLatestMs = Math.min(earliestForecastDataDate.getTime() + (autoZoomDuration / 2), latestForecastDataDate.getTime()); + rangeEarliestMs = Math.max(rangeLatestMs - autoZoomDuration, earliestDataDate.getTime()); + } else { + // Returns the range that shows the most recent data at bucket span granularity. + rangeLatestMs = latestDataDate.getTime() + contextAggregationInterval.asMilliseconds(); + rangeEarliestMs = Math.max(earliestDataDate.getTime(), rangeLatestMs - autoZoomDuration); + } + + return [new Date(rangeEarliestMs), new Date(rangeLatestMs)]; +} + +export function calculateInitialFocusRange(zoomState, contextAggregationInterval, timefilter) { + if (zoomState !== undefined) { + // Check that the zoom times are valid. + // zoomFrom must be at or after context chart search bounds earliest, + // zoomTo must be at or before context chart search bounds latest. + const zoomFrom = moment(zoomState.from, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); + const zoomTo = moment(zoomState.to, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); + const bounds = timefilter.getActiveBounds(); + const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, true); + const earliest = searchBounds.min; + const latest = searchBounds.max; + + if (zoomFrom.isValid() && zoomTo.isValid && + zoomTo.isAfter(zoomFrom) && + zoomFrom.isBetween(earliest, latest, null, '[]') && + zoomTo.isBetween(earliest, latest, null, '[]')) { + return [zoomFrom.toDate(), zoomTo.toDate()]; + } + } + + return undefined; +} + +export function getAutoZoomDuration(jobs, selectedJob) { + // Calculate the 'auto' zoom duration which shows data at bucket span granularity. + // Get the minimum bucket span of selected jobs. + // TODO - only look at jobs for which data has been returned? + const bucketSpanSeconds = _.find(jobs, { 'id': selectedJob.job_id }).bucketSpanSeconds; + + // In most cases the duration can be obtained by simply multiplying the points target + // Check that this duration returns the bucket span when run back through the + // TimeBucket interval calculation. + let autoZoomDuration = (bucketSpanSeconds * 1000) * (CHARTS_POINT_TARGET - 1); + + // Use a maxBars of 10% greater than the target. + const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET); + const buckets = new TimeBuckets(); + buckets.setInterval('auto'); + buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET)); + buckets.setMaxBars(maxBars); + + // Set bounds from 'now' for testing the auto zoom duration. + const nowMs = new Date().getTime(); + const max = moment(nowMs); + const min = moment(nowMs - autoZoomDuration); + buckets.setBounds({ min, max }); + + const calculatedInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); + const calculatedIntervalSecs = calculatedInterval.asSeconds(); + if (calculatedIntervalSecs !== bucketSpanSeconds) { + // If we haven't got the span back, which may occur depending on the 'auto' ranges + // used in TimeBuckets and the bucket span of the job, then multiply by the ratio + // of the bucket span to the calculated interval. + autoZoomDuration = autoZoomDuration * (bucketSpanSeconds / calculatedIntervalSecs); + } + + return autoZoomDuration; +} diff --git a/x-pack/legacy/plugins/ml/public/util/__snapshots__/observable_utils.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/util/__snapshots__/observable_utils.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__snapshots__/observable_utils.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/util/__snapshots__/observable_utils.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/app_state_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/app_state_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/app_state_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/app_state_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/calc_auto_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/calc_auto_interval.js diff --git a/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js new file mode 100644 index 0000000000000..9c107034359d6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import $ from 'jquery'; +import d3 from 'd3'; +import expect from '@kbn/expect'; +import { + chartLimits, + filterAxisLabels, + getChartType, + numTicks, + showMultiBucketAnomalyMarker, + showMultiBucketAnomalyTooltip, +} from '../chart_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; +import { CHART_TYPE } from '../../explorer/explorer_constants'; + +describe('ML - chart utils', () => { + + describe('chartLimits', () => { + + it('returns NaN when called without data', () => { + const limits = chartLimits(); + expect(limits.min).to.be.NaN; + expect(limits.max).to.be.NaN; + }); + + it('returns {max: 625736376, min: 201039318} for some test data', () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1, + actual: [228243469], typical: [133107.7703441773] + }, + { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: 625736376, anomalyScore: 97.32085, numberOfCauses: 1, + actual: [625736376], typical: [132830.424736973] + }, + { + date: new Date('2017-02-23T13:00:00.000Z'), + value: 201039318, anomalyScore: 59.83488, numberOfCauses: 1, + actual: [201039318], typical: [132739.5267403542] + } + ]; + + const limits = chartLimits(data); + + // {max: 625736376, min: 201039318} + expect(limits.min).to.be(201039318); + expect(limits.max).to.be(625736376); + }); + + it('adds 5% padding when min/max are the same, e.g. when there\'s only one data point', () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 100, anomalyScore: 50, numberOfCauses: 1, + actual: [100], typical: [100] + } + ]; + + const limits = chartLimits(data); + expect(limits.min).to.be(95); + expect(limits.max).to.be(105); + }); + + it('returns minimum of 0 when data includes an anomaly for missing data', () => { + const data = [ + { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: null, anomalyScore: 97.32085, + actual: [0], typical: [22.2] + }, + { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, + { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, + { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 } + ]; + + const limits = chartLimits(data); + expect(limits.min).to.be(0); + expect(limits.max).to.be(24.4); + }); + + }); + + describe('filterAxisLabels', () => { + + it('throws an error when called without arguments', () => { + expect(() => filterAxisLabels()).to.throwError(); + }); + + it('filters axis labels', () => { + // this provides a dummy structure of axis labels. + // the first one should always be filtered because it overflows on the + // left side of the axis. the last one should be filtered based on the + // given width parameter when doing the test calls. + $('body').append(` + + + + 06:00 + + + 12:00 + + + 18:00 + + + 00:00 + + + + `); + + const selector = '#filterAxisLabels .x.axis'; + + // given this width, the last tick should not be removed + filterAxisLabels(d3.selectAll(selector), 1000); + expect(d3.selectAll(selector + ' .tick text').size()).to.be(3); + + // given this width, the last tick should be removed + filterAxisLabels(d3.selectAll(selector), 790); + expect(d3.selectAll(selector + ' .tick text').size()).to.be(2); + + // clean up + $('#filterAxisLabels').remove(); + }); + + }); + + describe('getChartType', () => { + + const singleMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [], + }; + + const multiMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [ + { + fieldName: 'airline', + fieldValue: 'AAL', + fieldType: 'partition', + } + ], + }; + + const populationConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'http.response.body.bytes', + entityFields: [ + { + fieldName: 'source.ip', + fieldValue: '10.11.12.13', + fieldType: 'over', + } + ], + }; + + const rareConfig = { + metricFunction: 'count', + functionDescription: 'rare', + entityFields: [ + { + fieldName: 'http.response.status_code', + fieldValue: '404', + fieldType: 'by', + } + ], + }; + + const varpModelPlotConfig = { + metricFunction: null, + functionDescription: 'varp', + fieldName: 'NetworkOut', + entityFields: [ + { + fieldName: 'instance', + fieldValue: 'i-ef74d410', + fieldType: 'over', + } + ], + }; + + const overScriptFieldModelPlotConfig = { + metricFunction: 'count', + functionDescription: 'count', + fieldName: 'highest_registered_domain', + entityFields: [ + { + fieldName: 'highest_registered_domain', + fieldValue: 'elastic.co', + fieldType: 'over', + } + ], + datafeedConfig: { + script_fields: { + highest_registered_domain: { + script: { + source: 'return domainSplit(doc[\'query\'].value, params).get(1);', + lang: 'painless' + }, + ignore_failure: false + } + } + } + }; + + it('returns single metric chart type as expected for configs', () => { + expect(getChartType(singleMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(multiMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(varpModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(overScriptFieldModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); + + }); + + it('returns event distribution chart type as expected for configs', () => { + expect(getChartType(rareConfig)).to.be(CHART_TYPE.EVENT_DISTRIBUTION); + }); + + it('returns population distribution chart type as expected for configs', () => { + expect(getChartType(populationConfig)).to.be(CHART_TYPE.POPULATION_DISTRIBUTION); + }); + + }); + + describe('numTicks', () => { + + it('returns 10 for 1000', () => { + expect(numTicks(1000)).to.be(10); + }); + + }); + + describe('showMultiBucketAnomalyMarker', () => { + + it('returns true for points with multiBucketImpact at or above medium impact', () => { + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true); + }); + + it('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyMarker({})).to.be(false); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(false); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false); + }); + + }); + + describe('showMultiBucketAnomalyTooltip', () => { + + it('returns true for points with multiBucketImpact at or above low impact', () => { + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(true); + }); + + it('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyTooltip({})).to.be(false); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false); + }); + + }); + +}); diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/string_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/string_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/string_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/string_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/app_state_utils.js b/x-pack/legacy/plugins/ml/public/application/util/app_state_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/app_state_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/app_state_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/application/util/calc_auto_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js rename to x-pack/legacy/plugins/ml/public/application/util/calc_auto_interval.js diff --git a/x-pack/legacy/plugins/ml/public/util/chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/util/chart_config_builder.js rename to x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js index 1529ea868d4e6..844d46001b8e7 100644 --- a/x-pack/legacy/plugins/ml/public/util/chart_config_builder.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js @@ -13,7 +13,7 @@ import _ from 'lodash'; -import { mlFunctionToESAggregation } from '../../common/util/job_utils'; +import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; // Builds the basic configuration to plot a chart of the source data // analyzed by the the detector at the given index from the specified ML job. diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js new file mode 100644 index 0000000000000..8aa933eb5e53f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -0,0 +1,407 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 d3 from 'd3'; +import { calculateTextWidth } from './string_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +import moment from 'moment'; +import rison from 'rison-node'; + +import chrome from 'ui/chrome'; +import { timefilter } from 'ui/timefilter'; + +import { CHART_TYPE } from '../explorer/explorer_constants'; + +export const LINE_CHART_ANOMALY_RADIUS = 7; +export const MULTI_BUCKET_SYMBOL_SIZE = 100; // In square pixels for use with d3 symbol.size +export const SCHEDULED_EVENT_SYMBOL_HEIGHT = 5; + +const MAX_LABEL_WIDTH = 100; + +export function chartLimits(data = []) { + const domain = d3.extent(data, (d) => { + let metricValue = d.value; + if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { + // If an anomaly coincides with a gap in the data, use the anomaly actual value. + metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; + } + return metricValue; + }); + const limits = { max: domain[1], min: domain[0] }; + + if (limits.max === limits.min) { + limits.max = d3.max(data, (d) => { + if (d.typical) { + return Math.max(d.value, d.typical); + } else { + // If analysis with by and over field, and more than one cause, + // there will be no actual and typical value. + // TODO - produce a better visual for population analyses. + return d.value; + } + }); + limits.min = d3.min(data, (d) => { + if (d.typical) { + return Math.min(d.value, d.typical); + } else { + // If analysis with by and over field, and more than one cause, + // there will be no actual and typical value. + // TODO - produce a better visual for population analyses. + return d.value; + } + }); + } + + // add padding of 5% of the difference between max and min + // if we ended up with the same value for both of them + if (limits.max === limits.min) { + const padding = limits.max * 0.05; + limits.max += padding; + limits.min -= padding; + } + + return limits; +} + +export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, radius = 1.5) { + // We need to do this because when creating a line for a chart which has data gaps, + // if there are single datapoints without any valid data before and after them, + // the lines created by using d3...defined() do not contain these data points. + // So this function adds additional circle elements to display the single + // datapoints in additional to the line created for the chart. + + // first reduce the dataset to data points + // where the previous and next one don't contain any data + const dotsData = data.reduce((p, c, i) => { + const previous = data[i - 1]; + const next = data[i + 1]; + if ( + (typeof previous === 'undefined' || (previous && previous.value === null)) && + c.value !== null && + (typeof next === 'undefined' || (next && next.value === null)) + ) { + p.push(c); + } + return p; + }, []); + + // check if `g.values-dots` already exists, if not create it + // in both cases assign the element to `dotGroup` + const dotGroup = (lineChartGroup.select('.values-dots').empty()) + ? lineChartGroup.append('g').classed('values-dots', true) + : lineChartGroup.select('.values-dots'); + + // use d3's enter/update/exit pattern to render the dots + const dots = dotGroup.selectAll('circle').data(dotsData); + + dots.enter().append('circle') + .attr('r', radius); + + dots + .attr('cx', lineChartValuesLine.x()) + .attr('cy', lineChartValuesLine.y()); + + dots.exit().remove(); +} + +// this replicates Kibana's filterAxisLabels() behavior +// which can be found in ui/vislib/lib/axis/axis_labels.js +// axis labels which overflow the chart's boundaries will be removed +export function filterAxisLabels(selection, chartWidth) { + if (selection === undefined || selection.selectAll === undefined) { + throw new Error('Missing selection parameter'); + } + + selection.selectAll('.tick text') + // don't refactor this to an arrow function because + // we depend on using `this` here. + .text(function () { + const parent = d3.select(this.parentNode); + const labelWidth = parent.node().getBBox().width; + const labelXPos = d3.transform(parent.attr('transform')).translate[0]; + const minThreshold = labelXPos - (labelWidth / 2); + const maxThreshold = labelXPos + (labelWidth / 2); + if (minThreshold >= 0 && maxThreshold <= chartWidth) { + return this.textContent; + } else { + parent.remove(); + } + }); +} + +// feature flags for chart types +const EVENT_DISTRIBUTION_ENABLED = true; +const POPULATION_DISTRIBUTION_ENABLED = true; + +// get the chart type based on its configuration +export function getChartType(config) { + let chartType = CHART_TYPE.SINGLE_METRIC; + if ( + EVENT_DISTRIBUTION_ENABLED && + config.functionDescription === 'rare' && + (config.entityFields.some(f => f.fieldType === 'over') === false) + ) { + chartType = CHART_TYPE.EVENT_DISTRIBUTION; + } else if ( + POPULATION_DISTRIBUTION_ENABLED && + config.functionDescription !== 'rare' && + config.entityFields.some(f => f.fieldType === 'over') && + config.metricFunction !== null // Event distribution chart relies on the ML function mapping to an ES aggregation + ) { + chartType = CHART_TYPE.POPULATION_DISTRIBUTION; + } + + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + // Check that the config does not use script fields defined in the datafeed config. + if (config.datafeedConfig !== undefined && config.datafeedConfig.script_fields !== undefined) { + const scriptFields = Object.keys(config.datafeedConfig.script_fields); + const checkFields = config.entityFields.map(entity => entity.fieldName); + if (config.metricFieldName) { + checkFields.push(config.metricFieldName); + } + const usesScriptFields = + (checkFields.find(fieldName => scriptFields.includes(fieldName)) !== undefined); + if (usesScriptFields === true) { + // Only single metric chart type supports query of model plot data. + chartType = CHART_TYPE.SINGLE_METRIC; + } + } + } + + return chartType; +} + +export function getExploreSeriesLink(series) { + // Open the Single Metric dashboard over the same overall bounds and + // zoomed in to the same time as the current chart. + const bounds = timefilter.getActiveBounds(); + const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z + const to = bounds.max.toISOString(); + + const zoomFrom = moment(series.plotEarliest).toISOString(); + const zoomTo = moment(series.plotLatest).toISOString(); + + // Pass the detector index and entity fields (i.e. by, over, partition fields) + // to identify the particular series to view. + // Initially pass them in the mlTimeSeriesExplorer part of the AppState. + // TODO - do we want to pass the entities via the filter? + const entityCondition = {}; + series.entityFields.forEach((entity) => { + entityCondition[entity.fieldName] = entity.fieldValue; + }); + + // Use rison to build the URL . + const _g = rison.encode({ + ml: { + jobIds: [series.jobId] + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0 + }, + time: { + from: from, + to: to, + mode: 'absolute' + } + }); + + const _a = rison.encode({ + mlTimeSeriesExplorer: { + zoom: { + from: zoomFrom, + to: zoomTo + }, + detectorIndex: series.detectorIndex, + entities: entityCondition, + }, + query: { + query_string: { + analyze_wildcard: true, + query: '*' + } + } + }); + + return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; +} + +export function showMultiBucketAnomalyMarker(point) { + // TODO - test threshold with real use cases + return (point.multiBucketImpact !== undefined && + point.multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM); +} + +export function showMultiBucketAnomalyTooltip(point) { + // TODO - test threshold with real use cases + return (point.multiBucketImpact !== undefined && + point.multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW); +} + +export function numTicks(axisWidth) { + return axisWidth / MAX_LABEL_WIDTH; +} + +export function numTicksForDateFormat(axisWidth, dateFormat) { + // Allow 1.75 times the width of a formatted date per tick for padding. + const tickWidth = calculateTextWidth(moment().format(dateFormat), false); + return axisWidth / (1.75 * tickWidth); +} + +const TICK_DIRECTION = { + NEXT: 'next', + PREVIOUS: 'previous' +}; + +// Based on a fixed starting timestamp and an interval, get tick values within +// the bounds of earliest and latest. This is useful for the Anomaly Explorer Charts +// to align axis ticks with the gray area resembling the swimlane cell selection. +export function getTickValues(startTimeMs, tickInterval, earliest, latest) { + // A tickInterval equal or smaller than 0 would trigger a call stack exception, + // so we're trying to catch that before it happens. + if (tickInterval <= 0) { + throw Error('tickInterval must be larger than 0.'); + } + + const tickValues = [startTimeMs]; + + function addTicks(ts, operator) { + let newTick; + let addAnotherTick; + + switch (operator) { + case TICK_DIRECTION.PREVIOUS: + newTick = ts - tickInterval; + addAnotherTick = newTick >= earliest; + break; + case TICK_DIRECTION.NEXT: + newTick = ts + tickInterval; + addAnotherTick = newTick <= latest; + break; + } + + if (addAnotherTick) { + tickValues.push(newTick); + addTicks(newTick, operator); + } + } + + addTicks(startTimeMs, TICK_DIRECTION.PREVIOUS); + addTicks(startTimeMs, TICK_DIRECTION.NEXT); + + tickValues.sort(); + + return tickValues; +} + +const LABEL_WRAP_THRESHOLD = 60; + +// Checks if the string length of a chart label (detector description +// and entity fields) is above LABEL_WRAP_THRESHOLD. +export function isLabelLengthAboveThreshold({ detectorLabel, entityFields }) { + const labelLength = (detectorLabel.length + entityFields.map(d => `${d.fieldName} ${d.fieldValue}`).join(' ').length); + return (labelLength > LABEL_WRAP_THRESHOLD); +} + +// To get xTransform it would be nicer to use d3.transform, but that doesn't play well with JSDOM. +// So this uses a regex variant because we definitely want test coverage for the label removal. +// Once JSDOM supports SVGAnimatedTransformList we can use this simpler inline version: +// const xTransform = d3.transform(tick.attr('transform')).translate[0]; +export function getXTransform(t) { + const regexResult = /translate\(\s*([^\s,)]+)([ ,]([^\s,)]+))?\)/.exec(t); + if (Array.isArray(regexResult) && regexResult.length >= 2) { + return Number(regexResult[1]); + } + + // fall back to NaN if regex didn't return any results. + return NaN; +} + +// This removes overlapping x-axis labels by starting off from a specific label +// that is required/wanted to show up. The code then traverses to both sides along the axis +// and decides which labels to keep or remove. All vertical tick lines will be kept visible, +// but those which still have their text label will be emphasized using the ml-tick-emphasis class. +export function removeLabelOverlap(axis, startTimeMs, tickInterval, width) { + // Put emphasis on all tick lines, will again de-emphasize the + // ones where we remove the label in the next steps. + axis.selectAll('g.tick').select('line').classed('ml-tick-emphasis', true); + + function getNeighborTickFactory(operator) { + return function (ts) { + switch (operator) { + case TICK_DIRECTION.PREVIOUS: + return ts - tickInterval; + case TICK_DIRECTION.NEXT: + return ts + tickInterval; + } + }; + } + + function getTickDataFactory(operator) { + const getNeighborTick = getNeighborTickFactory(operator); + const fn = function (ts) { + const filteredTicks = axis.selectAll('.tick').filter(d => d === ts); + + if (filteredTicks.length === 0 || filteredTicks[0].length === 0) { + return false; + } + + const tick = d3.selectAll(filteredTicks[0]); + const textNode = tick.select('text').node(); + + if (textNode === null) { + return fn(getNeighborTick(ts)); + } + + const tickWidth = textNode.getBBox().width; + const padding = 15; + const xTransform = getXTransform(tick.attr('transform')); + const xMinOffset = xTransform - (tickWidth / 2 + padding); + const xMaxOffset = xTransform + (tickWidth / 2 + padding); + + return { + tick, + ts, + xMinOffset, + xMaxOffset + }; + }; + return fn; + } + + function checkTicks(ts, operator) { + const getTickData = getTickDataFactory(operator); + const currentTickData = getTickData(ts); + + if (currentTickData === false) { + return; + } + + const getNeighborTick = getNeighborTickFactory(operator); + const newTickData = getTickData(getNeighborTick(ts)); + + if (newTickData !== false) { + if ( + newTickData.xMinOffset < 0 || + newTickData.xMaxOffset > width || + (newTickData.xMaxOffset > currentTickData.xMinOffset && operator === TICK_DIRECTION.PREVIOUS) || + (newTickData.xMinOffset < currentTickData.xMaxOffset && operator === TICK_DIRECTION.NEXT) + ) { + newTickData.tick.select('text').remove(); + newTickData.tick.select('line').classed('ml-tick-emphasis', false); + checkTicks(currentTickData.ts, operator); + } else { + checkTicks(newTickData.ts, operator); + } + } + } + + checkTicks(startTimeMs, TICK_DIRECTION.PREVIOUS); + checkTicks(startTimeMs, TICK_DIRECTION.NEXT); +} diff --git a/x-pack/legacy/plugins/ml/public/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/util/chart_utils.test.js rename to x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js index 6d13f1bc66808..a229113826a2e 100644 --- a/x-pack/legacy/plugins/ml/public/util/chart_utils.test.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js @@ -6,12 +6,6 @@ import seriesConfig from '../explorer/explorer_charts/__mocks__/mock_series_config_filebeat'; -jest.mock('ui/registry/field_formats', () => ({ - fieldFormats: { - getDefaultInstance: jest.fn(), - }, -})); - jest.mock('ui/timefilter', () => { const dateMath = require('@elastic/datemath'); let _time = undefined; diff --git a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/util/custom_url_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts index ff97c3fffbf1c..6684ad3fa3e9b 100644 --- a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -10,12 +10,12 @@ import { isValidLabel, isValidTimeRange, } from './custom_url_utils'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { CustomUrlAnomalyRecordDoc, KibanaUrlConfig, UrlConfig, -} from '../../common/types/custom_urls'; +} from '../../../common/types/custom_urls'; describe('ML - custom URL utils', () => { const TEST_DOC: AnomalyRecordDoc = { diff --git a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts similarity index 97% rename from x-pack/legacy/plugins/ml/public/util/custom_url_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts index 0d21aed222cad..1a15f607e13c2 100644 --- a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts @@ -9,14 +9,14 @@ import { get, flow } from 'lodash'; import moment from 'moment'; -import { parseInterval } from '../../common/util/parse_interval'; +import { parseInterval } from '../../../common/util/parse_interval'; import { escapeForElasticsearchQuery, replaceStringTokens } from './string_utils'; import { UrlConfig, KibanaUrlConfig, CustomUrlAnomalyRecordDoc, -} from '../../common/types/custom_urls'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +} from '../../../common/types/custom_urls'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; // Value of custom_url time_range property indicating drilldown time range is calculated automatically // depending on the context in which the URL is being opened. diff --git a/x-pack/legacy/plugins/ml/public/util/date_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/date_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/date_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/date_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/date_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/date_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/date_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/date_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/field_types_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts similarity index 95% rename from x-pack/legacy/plugins/ml/public/util/field_types_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts index 3ca1adeb08f95..2abb8097598d2 100644 --- a/x-pack/legacy/plugins/ml/public/util/field_types_utils.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts @@ -5,8 +5,8 @@ */ import { FieldType } from 'ui/index_patterns'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { ML_JOB_FIELD_TYPES } from './../../common/constants/field_types'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; import { kbnTypeToMLJobType, getMLJobTypeAriaLabel, diff --git a/x-pack/legacy/plugins/ml/public/util/field_types_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts similarity index 94% rename from x-pack/legacy/plugins/ml/public/util/field_types_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts index e97ff11bc2bb7..e2b876aa8dbcd 100644 --- a/x-pack/legacy/plugins/ml/public/util/field_types_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { FieldType } from 'ui/index_patterns'; -import { ML_JOB_FIELD_TYPES } from './../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; // convert kibana types to ML Job types // this is needed because kibana types only have string and not text and keyword. diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts similarity index 93% rename from x-pack/legacy/plugins/ml/public/util/index_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 5c15cdd6b8df0..f25821e8ca1ca 100644 --- a/x-pack/legacy/plugins/ml/public/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import chrome from 'ui/chrome'; -import { SavedSearchLoader } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; -import { start as data } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; +import { SavedSearchLoader } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { start as data } from '../../../../../../../src/legacy/core_plugins/data/public/legacy'; type IndexPatternSavedObject = SimpleSavedObject; diff --git a/x-pack/legacy/plugins/ml/public/util/inherits.js b/x-pack/legacy/plugins/ml/public/application/util/inherits.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/inherits.js rename to x-pack/legacy/plugins/ml/public/application/util/inherits.js diff --git a/x-pack/legacy/plugins/ml/public/util/ml_error.js b/x-pack/legacy/plugins/ml/public/application/util/ml_error.js similarity index 90% rename from x-pack/legacy/plugins/ml/public/util/ml_error.js rename to x-pack/legacy/plugins/ml/public/application/util/ml_error.js index d5a3507ffaa15..2d319a395af54 100644 --- a/x-pack/legacy/plugins/ml/public/util/ml_error.js +++ b/x-pack/legacy/plugins/ml/public/application/util/ml_error.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KbnError } from '../../../../../../src/plugins/kibana_utils/public'; +import { KbnError } from '../../../../../../../src/plugins/kibana_utils/public'; export class MLRequestFailure extends KbnError { // takes an Error object and and optional response object diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/object_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/object_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/object_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/object_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/object_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/object_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/observable_utils.test.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.test.tsx similarity index 95% rename from x-pack/legacy/plugins/ml/public/util/observable_utils.test.tsx rename to x-pack/legacy/plugins/ml/public/application/util/observable_utils.test.tsx index f256a1470f7f6..c95824fc5dc4d 100644 --- a/x-pack/legacy/plugins/ml/public/util/observable_utils.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.test.tsx @@ -20,7 +20,7 @@ describe('observable_utils', () => { const observable$ = new BehaviorSubject('initial text'); // a simple stateless component that just renders some text - const TestComponent: React.SFC = ({ testProp }) => { + const TestComponent: React.FC = ({ testProp }) => { return {testProp}; }; diff --git a/x-pack/legacy/plugins/ml/public/util/observable_utils.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx similarity index 96% rename from x-pack/legacy/plugins/ml/public/util/observable_utils.tsx rename to x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx index bdc6c70aac749..7f1fc366bc5bb 100644 --- a/x-pack/legacy/plugins/ml/public/util/observable_utils.tsx +++ b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx @@ -7,7 +7,7 @@ import React, { Component, ComponentType } from 'react'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { Dictionary } from '../../common/types/common'; +import { Dictionary } from '../../../common/types/common'; // Sets up a ObservableComponent which subscribes to given observable updates and // and passes them on as prop values to the given WrappedComponent. diff --git a/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/recently_accessed.ts rename to x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts diff --git a/x-pack/legacy/plugins/ml/public/util/string_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/util/string_utils.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/string_utils.d.ts rename to x-pack/legacy/plugins/ml/public/application/util/string_utils.d.ts diff --git a/x-pack/legacy/plugins/ml/public/util/string_utils.js b/x-pack/legacy/plugins/ml/public/application/util/string_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/string_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/string_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/test_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/test_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/test_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts diff --git a/x-pack/legacy/plugins/ml/public/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js similarity index 97% rename from x-pack/legacy/plugins/ml/public/util/time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.js index 6933ee6935e80..45181ab9f4f62 100644 --- a/x-pack/legacy/plugins/ml/public/util/time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js @@ -9,10 +9,11 @@ import _ from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import chrome from 'ui/chrome'; -import { fieldFormats } from 'ui/registry/field_formats'; +import { npStart } from 'ui/new_platform'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; -import { parseInterval } from '../../common/util/parse_interval'; +import { parseInterval } from '../../../common/util/parse_interval'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/data/public'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. @@ -316,7 +317,8 @@ TimeBuckets.prototype.getScaledDateFormat = function () { }; TimeBuckets.prototype.getScaledDateFormatter = function () { - const DateFieldFormat = fieldFormats.getType('date'); + const fieldFormats = npStart.plugins.data.fieldFormats; + const DateFieldFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE); return new DateFieldFormat({ pattern: this.getScaledDateFormat() }, getConfig); diff --git a/x-pack/legacy/plugins/ml/public/util/url_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/url_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/url_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/url_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/url_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/url_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/url_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/url_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.tsx b/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.tsx deleted file mode 100644 index 52224e5fc3fb6..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.tsx +++ /dev/null @@ -1,91 +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. - */ - -/* - * React component for listing pairs of information about the detector for which - * rules are being edited. - */ - -import React from 'react'; - -import { EuiDescriptionList } from '@elastic/eui'; - -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { Annotation } from '../../../../common/types/annotations'; -import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; - -interface Props { - annotation: Annotation; - intl: InjectedIntl; -} - -export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props) => { - const listItems = [ - { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', - defaultMessage: 'Job ID', - }), - description: annotation.job_id, - }, - { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', - defaultMessage: 'Start', - }), - description: formatHumanReadableDateTimeSeconds(annotation.timestamp), - }, - ]; - - if (annotation.end_timestamp !== undefined) { - listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', - defaultMessage: 'End', - }), - description: formatHumanReadableDateTimeSeconds(annotation.end_timestamp), - }); - } - - if (annotation.create_time !== undefined && annotation.modified_time !== undefined) { - listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle', - defaultMessage: 'Created', - }), - description: formatHumanReadableDateTimeSeconds(annotation.create_time), - }); - listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdByTitle', - defaultMessage: 'Created by', - }), - description: annotation.create_username, - }); - listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.lastModifiedTitle', - defaultMessage: 'Last modified', - }), - description: formatHumanReadableDateTimeSeconds(annotation.modified_time), - }); - listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle', - defaultMessage: 'Modified by', - }), - description: annotation.modified_username, - }); - } - - return ( - - ); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx b/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx deleted file mode 100644 index 871dcd74d0907..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx +++ /dev/null @@ -1,55 +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 { injectObservablesAsProps } from '../../../util/observable_utils'; -import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; - -import React, { ComponentType } from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { Annotation } from '../../../../common/types/annotations'; -import { annotation$ } from '../../../services/annotations_service'; - -import { AnnotationFlyout } from './index'; - -describe('AnnotationFlyout', () => { - test('Initialization.', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); - }); - - test('Update button is disabled with empty annotation', () => { - const annotation = mockAnnotations[1] as Annotation; - annotation$.next(annotation); - - // injectObservablesAsProps wraps the observable in a new component - const ObservableComponent = injectObservablesAsProps( - { annotation: annotation$ }, - (AnnotationFlyout as any) as ComponentType - ); - - const wrapper = mountWithIntl(); - const updateBtn = wrapper.find('EuiButton').first(); - expect(updateBtn.prop('isDisabled')).toEqual(true); - }); - - test('Error displayed and update button displayed if annotation text is longer than max chars', () => { - const annotation = mockAnnotations[2] as Annotation; - annotation$.next(annotation); - - // injectObservablesAsProps wraps the observable in a new component - const ObservableComponent = injectObservablesAsProps( - { annotation: annotation$ }, - (AnnotationFlyout as any) as ComponentType - ); - - const wrapper = mountWithIntl(); - const updateBtn = wrapper.find('EuiButton').first(); - expect(updateBtn.prop('isDisabled')).toEqual(true); - - expect(wrapper.find('EuiFormErrorText')).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.tsx deleted file mode 100644 index 586e503632eb9..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.tsx +++ /dev/null @@ -1,350 +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 React, { Component, ComponentType, Fragment, ReactNode } from 'react'; -import * as Rx from 'rxjs'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiFormRow, - EuiSpacer, - EuiTextArea, - EuiTitle, -} from '@elastic/eui'; - -import { CommonProps } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { InjectedIntlProps } from 'react-intl'; -import { toastNotifications } from 'ui/notify'; -import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../common/constants/annotations'; -import { - annotation$, - annotationsRefresh$, - AnnotationState, -} from '../../../services/annotations_service'; -import { injectObservablesAsProps } from '../../../util/observable_utils'; -import { AnnotationDescriptionList } from '../annotation_description_list'; -import { DeleteAnnotationModal } from '../delete_annotation_modal'; - -import { ml } from '../../../services/ml_api_service'; - -interface Props { - annotation: AnnotationState; -} - -interface State { - isDeleteModalVisible: boolean; -} - -class AnnotationFlyoutIntl extends Component { - public state: State = { - isDeleteModalVisible: false, - }; - - public annotationSub: Rx.Subscription | null = null; - - public annotationTextChangeHandler = (e: React.ChangeEvent) => { - if (this.props.annotation === null) { - return; - } - - annotation$.next({ - ...this.props.annotation, - annotation: e.target.value, - }); - }; - - public cancelEditingHandler = () => { - annotation$.next(null); - }; - - public deleteConfirmHandler = () => { - this.setState({ isDeleteModalVisible: true }); - }; - - public deleteHandler = async () => { - const { annotation, intl } = this.props; - - if (annotation === null) { - return; - } - - try { - await ml.annotations.deleteAnnotation(annotation._id); - toastNotifications.addSuccess( - intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.deletedAnnotationNotificationMessage', - defaultMessage: 'Deleted annotation for job with ID {jobId}.', - }, - { jobId: annotation.job_id } - ) - ); - } catch (err) { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithDeletingAnnotationNotificationErrorMessage', - defaultMessage: - 'An error occurred deleting the annotation for job with ID {jobId}: {error}', - }, - { jobId: annotation.job_id, error: JSON.stringify(err) } - ) - ); - } - - this.closeDeleteModal(); - annotation$.next(null); - annotationsRefresh$.next(true); - }; - - public closeDeleteModal = () => { - this.setState({ isDeleteModalVisible: false }); - }; - - public validateAnnotationText = () => { - // Validates the entered text, returning an array of error messages - // for display in the form. An empty array is returned if the text is valid. - const { annotation, intl } = this.props; - const errors: string[] = []; - if (annotation === null) { - return errors; - } - - if (annotation.annotation.trim().length === 0) { - errors.push( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError', - defaultMessage: 'Enter annotation text', - }) - ); - } - - const textLength = annotation.annotation.length; - if (textLength > ANNOTATION_MAX_LENGTH_CHARS) { - const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS; - errors.push( - intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError', - defaultMessage: - '{charsOver, number} {charsOver, plural, one {character} other {characters}} above maximum length of {maxChars}', - }, - { - maxChars: ANNOTATION_MAX_LENGTH_CHARS, - charsOver, - } - ) - ); - } - - return errors; - }; - - public saveOrUpdateAnnotation = () => { - const { annotation, intl } = this.props; - - if (annotation === null) { - return; - } - - annotation$.next(null); - - ml.annotations - .indexAnnotation(annotation) - .then(() => { - annotationsRefresh$.next(true); - if (typeof annotation._id === 'undefined') { - toastNotifications.addSuccess( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.addedAnnotationNotificationMessage', - defaultMessage: 'Added an annotation for job with ID {jobId}.', - }, - { jobId: annotation.job_id } - ) - ); - } else { - toastNotifications.addSuccess( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.updatedAnnotationNotificationMessage', - defaultMessage: 'Updated annotation for job with ID {jobId}.', - }, - { jobId: annotation.job_id } - ) - ); - } - }) - .catch(resp => { - if (typeof annotation._id === 'undefined') { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithCreatingAnnotationNotificationErrorMessage', - defaultMessage: - 'An error occurred creating the annotation for job with ID {jobId}: {error}', - }, - { jobId: annotation.job_id, error: JSON.stringify(resp) } - ) - ); - } else { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithUpdatingAnnotationNotificationErrorMessage', - defaultMessage: - 'An error occurred updating the annotation for job with ID {jobId}: {error}', - }, - { jobId: annotation.job_id, error: JSON.stringify(resp) } - ) - ); - } - }); - }; - - public render(): ReactNode { - const { annotation, intl } = this.props; - const { isDeleteModalVisible } = this.state; - - if (annotation === null) { - return null; - } - - const isExistingAnnotation = typeof annotation._id !== 'undefined'; - - // Check the length of the text is within the max length limit, - // and warn if the length is approaching the limit. - const validationErrors = this.validateAnnotationText(); - const isInvalid = validationErrors.length > 0; - const lengthRatioToShowWarning = 0.95; - let helpText = null; - if ( - isInvalid === false && - annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning - ) { - helpText = intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning', - defaultMessage: - '{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining', - }, - { charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length } - ); - } - - return ( - - - - -

- {isExistingAnnotation ? ( - - ) : ( - - )} -

-
-
- - - - - } - fullWidth - helpText={helpText} - isInvalid={isInvalid} - error={validationErrors} - > - - - - - - - - - - - - {isExistingAnnotation && ( - - - - )} - - - - {isExistingAnnotation ? ( - - ) : ( - - )} - - - - -
- -
- ); - } -} - -export const AnnotationFlyout = injectObservablesAsProps( - { annotation: annotation$ }, - (injectI18n(AnnotationFlyoutIntl) as any) as ComponentType -); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/delete_annotation_modal/index.tsx b/x-pack/legacy/plugins/ml/public/components/annotations/delete_annotation_modal/index.tsx deleted file mode 100644 index 32e40b6edbed3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/delete_annotation_modal/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; - -import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - cancelAction: () => void; - deleteAction: () => void; - isVisible: boolean; -} - -export const DeleteAnnotationModal: React.SFC = ({ - cancelAction, - deleteAction, - isVisible, -}) => { - return ( - - {isVisible === true && ( - - - } - onCancel={cancelAction} - onConfirm={deleteAction} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - className="eui-textBreakWord" - /> - - )} - - ); -}; - -DeleteAnnotationModal.propTypes = { - cancelAction: PropTypes.func.isRequired, - deleteAction: PropTypes.func.isRequired, - isVisible: PropTypes.bool.isRequired, -}; diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/utils.js deleted file mode 100644 index 16e4a563c33ae..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/utils.js +++ /dev/null @@ -1,115 +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 { npStart } from 'ui/new_platform'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; - -const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); - -export async function getSuggestions( - query, - selectionStart, - indexPattern, - boolFilter -) { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [indexPattern], - boolFilter - }); - return getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart - }); -} - -function convertKueryToEsQuery(kuery, indexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); -} -// Recommended by MDN for escaping user input to be treated as a literal string within a regular expression -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions -export function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -export function escapeParens(string) { - return string.replace(/[()]/g, '\\$&'); -} - -export function escapeDoubleQuotes(string) { - return string.replace(/\"/g, '\\$&'); -} - -export function getKqlQueryValues(inputValue, indexPattern) { - const ast = fromKueryExpression(inputValue); - const isAndOperator = (ast.function === 'and'); - const query = convertKueryToEsQuery(inputValue, indexPattern); - const filteredFields = []; - - if (!query) { - return; - } - - // if ast.type == 'function' then layout of ast.arguments: - // [{ arguments: [ { type: 'literal', value: 'AAL' } ] },{ arguments: [ { type: 'literal', value: 'AAL' } ] }] - if (ast && Array.isArray(ast.arguments)) { - - ast.arguments.forEach((arg) => { - if (arg.arguments !== undefined) { - arg.arguments.forEach((nestedArg) => { - if (typeof nestedArg.value === 'string') { - filteredFields.push(nestedArg.value); - } - }); - } else if (typeof arg.value === 'string') { - filteredFields.push(arg.value); - } - }); - - } - - return { - filterQuery: query, - filteredFields, - queryString: inputValue, - isAndOperator, - tableQueryString: inputValue - }; -} - -export function getQueryPattern(fieldName, fieldValue) { - const sanitizedFieldName = escapeRegExp(fieldName); - const sanitizedFieldValue = escapeRegExp(fieldValue); - - return new RegExp(`(${sanitizedFieldName})\\s?:\\s?(")?(${sanitizedFieldValue})(")?`, 'i'); -} - -export function removeFilterFromQueryString(currentQueryString, fieldName, fieldValue) { - let newQueryString = ''; - // Remove the passed in fieldName and value from the existing filter - const queryPattern = getQueryPattern(fieldName, fieldValue); - newQueryString = currentQueryString.replace(queryPattern, ''); - // match 'and' or 'or' at the start/end of the string - const endPattern = /\s(and|or)\s*$/ig; - const startPattern = /^\s*(and|or)\s/ig; - // If string has a double operator (e.g. tag:thing or or tag:other) remove and replace with the first occurring operator - const invalidOperatorPattern = /\s+(and|or)\s+(and|or)\s+/ig; - newQueryString = newQueryString.replace(invalidOperatorPattern, ' $1 '); - // If string starts/ends with 'and' or 'or' remove that as that is illegal kuery syntax - newQueryString = newQueryString.replace(endPattern, ''); - newQueryString = newQueryString.replace(startPattern, ''); - - return newQueryString; -} diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/index.ts b/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/index.ts deleted file mode 100644 index 91bf31ea1e7ab..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ProgressBar, MlInMemoryTable } from './ml_in_memory_table'; -export * from './types'; diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/ml_in_memory_table.tsx b/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/ml_in_memory_table.tsx deleted file mode 100644 index d5316b22a6a6f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/ml_in_memory_table.tsx +++ /dev/null @@ -1,104 +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. - */ - -// This component extends EuiInMemoryTable with some -// fixes and TS specs until the changes become available upstream. - -import React, { Fragment } from 'react'; - -import { EuiProgress } from '@elastic/eui'; - -// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement -// of the table and doesn't play well with auto-refreshing. That's why we're displaying -// our own progress bar on top of the table. `EuiProgress` after `isLoading` displays -// the loading indicator. The variation after `!isLoading` displays an empty progress -// bar fixed to 0%. Without it, the display would vertically jump when showing/hiding -// the progress bar. -export const ProgressBar = ({ isLoading = false }) => { - return ( - - {isLoading && } - {!isLoading && ( - - )} - - ); -}; - -// copied from EUI to be available to the extended getDerivedStateFromProps() -function findColumnByProp(columns: any, prop: any, value: any) { - for (let i = 0; i < columns.length; i++) { - const column = columns[i]; - if (column[prop] === value) { - return column; - } - } -} - -// copied from EUI to be available to the extended getDerivedStateFromProps() -const getInitialSorting = (columns: any, sorting: any) => { - if (!sorting || !sorting.sort) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const { field: sortable, direction: sortDirection } = sorting.sort; - - // sortable could be a column's `field` or its `name` - // for backwards compatibility `field` must be checked first - let sortColumn = findColumnByProp(columns, 'field', sortable); - if (sortColumn == null) { - sortColumn = findColumnByProp(columns, 'name', sortable); - } - - if (sortColumn == null) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const sortName = sortColumn.name; - - return { - sortName, - sortDirection, - }; -}; - -import { MlInMemoryTableBasic } from './types'; - -export class MlInMemoryTable extends MlInMemoryTableBasic { - static getDerivedStateFromProps(nextProps: any, prevState: any) { - const derivedState = { - ...prevState.prevProps, - pageIndex: nextProps.pagination.initialPageIndex, - pageSize: nextProps.pagination.initialPageSize, - }; - - if (nextProps.items !== prevState.prevProps.items) { - Object.assign(derivedState, { - prevProps: { - items: nextProps.items, - }, - }); - } - - const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - Object.assign(derivedState, { - sortName, - sortDirection, - }); - } - return derivedState; - } -} diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts deleted file mode 100644 index fac0309e0aeb6..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts +++ /dev/null @@ -1,199 +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 { Component, HTMLAttributes, ReactElement, ReactNode } from 'react'; - -import { CommonProps, EuiInMemoryTable } from '@elastic/eui'; - -// At some point this could maybe solved with a generic . -type Item = any; - -// Not using an enum here because the original HorizontalAlignment is also a union type of string. -type HorizontalAlignment = 'left' | 'center' | 'right'; - -type SortableFunc = (item: Item) => any; -type Sortable = boolean | SortableFunc; -type DATA_TYPES = any; -type FooterFunc = (payload: { items: Item[]; pagination: any }) => ReactNode; -type RenderFunc = (value: any, record?: any) => ReactNode; -export interface FieldDataColumnType { - field: string; - name: ReactNode; - description?: string; - dataType?: DATA_TYPES; - width?: string; - sortable?: Sortable; - align?: HorizontalAlignment; - truncateText?: boolean; - render?: RenderFunc; - footer?: string | ReactElement | FooterFunc; - textOnly?: boolean; - 'data-test-subj'?: string; -} - -export interface ComputedColumnType { - render: RenderFunc; - name?: ReactNode; - description?: string; - sortable?: (item: Item) => any; - width?: string; - truncateText?: boolean; - 'data-test-subj'?: string; -} - -type ICON_TYPES = any; -type IconTypesFunc = (item: Item) => ICON_TYPES; // (item) => oneOf(ICON_TYPES) -type BUTTON_ICON_COLORS = any; -type ButtonIconColorsFunc = (item: Item) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS) -interface DefaultItemActionType { - type?: 'icon' | 'button'; - name: string; - description: string; - onClick?(item: Item): void; - href?: string; - target?: string; - available?(item: Item): boolean; - enabled?(item: Item): boolean; - isPrimary?: boolean; - icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon' - color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc; -} - -interface CustomItemActionType { - render(item: Item, enabled: boolean): ReactNode; - available?(item: Item): boolean; - enabled?(item: Item): boolean; - isPrimary?: boolean; -} - -export interface ExpanderColumnType { - align?: HorizontalAlignment; - width?: string; - isExpander: boolean; - render: RenderFunc; -} - -type SupportedItemActionType = DefaultItemActionType | CustomItemActionType; - -export interface ActionsColumnType { - actions: SupportedItemActionType[]; - name?: ReactNode; - description?: string; - width?: string; -} - -export type ColumnType = - | ActionsColumnType - | ComputedColumnType - | ExpanderColumnType - | FieldDataColumnType; - -type QueryType = any; - -interface Schema { - strict?: boolean; - fields?: Record; - flags?: string[]; -} - -interface SearchBoxConfigPropTypes { - placeholder?: string; - incremental?: boolean; - schema?: Schema; -} - -interface Box { - placeholder?: string; - incremental?: boolean; - // here we enable the user to just assign 'true' to the schema, in which case - // we will auto-generate it out of the columns configuration - schema?: boolean | SearchBoxConfigPropTypes['schema']; -} - -type SearchFiltersFiltersType = any; - -interface ExecuteQueryOptions { - defaultFields: string[]; - isClauseMatcher: () => void; - explain: boolean; -} - -type SearchType = - | boolean - | { - toolsLeft?: ReactNode; - toolsRight?: ReactNode; - defaultQuery?: QueryType; - box?: Box; - filters?: SearchFiltersFiltersType; - onChange?: (arg: any) => void; - executeQueryOptions?: ExecuteQueryOptions; - }; - -interface PageSizeOptions { - pageSizeOptions: number[]; -} -interface InitialPageOptions extends PageSizeOptions { - initialPageIndex: number; - initialPageSize: number; -} -type PaginationProp = boolean | PageSizeOptions | InitialPageOptions; - -export enum SORT_DIRECTION { - ASC = 'asc', - DESC = 'desc', -} -export type SortDirection = SORT_DIRECTION.ASC | SORT_DIRECTION.DESC; -export interface Sorting { - sort: { - field: string; - direction: SortDirection; - }; -} -export type SortingPropType = boolean | Sorting; - -type SelectionType = any; - -export interface OnTableChangeArg extends Sorting { - page: { index: number; size: number }; -} - -type ItemIdTypeFunc = (item: Item) => string; -type ItemIdType = - | string // the name of the item id property - | ItemIdTypeFunc; - -export type EuiInMemoryTableProps = CommonProps & { - columns: ColumnType[]; - hasActions?: boolean; - isExpandable?: boolean; - isSelectable?: boolean; - items?: Item[]; - loading?: boolean; - message?: HTMLAttributes; - error?: string; - compressed?: boolean; - search?: SearchType; - pagination?: PaginationProp; - sorting?: SortingPropType; - // Set `allowNeutralSort` to false to force column sorting. Defaults to true. - allowNeutralSort?: boolean; - responsive?: boolean; - selection?: SelectionType; - itemId?: ItemIdType; - itemIdToExpandedRowMap?: Record; - rowProps?: (item: Item) => void | Record; - cellProps?: () => void | Record; - onTableChange?: (arg: OnTableChangeArg) => void; -}; - -interface ComponentWithConstructor extends Component { - new (): Component; -} - -export const MlInMemoryTableBasic = (EuiInMemoryTable as any) as ComponentWithConstructor< - EuiInMemoryTableProps ->; diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/components/rule_editor/__tests__/utils.js deleted file mode 100644 index 88818a3c978b3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/__tests__/utils.js +++ /dev/null @@ -1,134 +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 { - isValidRule, - buildRuleDescription, - getAppliesToValueFromAnomaly, -} from '../utils'; -import { - ACTION, - APPLIES_TO, - OPERATOR, - FILTER_TYPE, -} from '../../../../common/constants/detector_rule'; - -describe('ML - rule editor utils', () => { - - const ruleWithCondition = { - actions: [ACTION.SKIP_RESULT], - conditions: [ - { - applies_to: APPLIES_TO.ACTUAL, - operator: OPERATOR.GREATER_THAN, - value: 10 - } - ] - }; - - const ruleWithScope = { - actions: [ACTION.SKIP_RESULT], - scope: { - instance: { - filter_id: 'test_aws_instances', - filter_type: FILTER_TYPE.INCLUDE, - enabled: true - } - } - }; - - const ruleWithConditionAndScope = { - actions: [ACTION.SKIP_RESULT], - conditions: [ - { - applies_to: APPLIES_TO.TYPICAL, - operator: OPERATOR.LESS_THAN, - value: 100 - } - ], - scope: { - instance: { - filter_id: 'test_aws_instances', - filter_type: FILTER_TYPE.EXCLUDE, - enabled: true - } - } - }; - - describe('isValidRule', () => { - - it('returns true for a rule with an action and a condition', () => { - expect(isValidRule(ruleWithCondition)).to.be(true); - }); - - it('returns true for a rule with an action and scope', () => { - expect(isValidRule(ruleWithScope)).to.be(true); - }); - - it('returns true for a rule with an action, scope and condition', () => { - expect(isValidRule(ruleWithConditionAndScope)).to.be(true); - }); - - it('returns false for a rule with no action', () => { - const ruleWithNoAction = { - actions: [], - conditions: [ - { - applies_to: APPLIES_TO.TYPICAL, - operator: OPERATOR.LESS_THAN, - value: 100 - } - ], - }; - - expect(isValidRule(ruleWithNoAction)).to.be(false); - }); - - it('returns false for a rule with no scope or conditions', () => { - const ruleWithNoScopeOrCondition = { - actions: [ACTION.SKIP_RESULT], - }; - - expect(isValidRule(ruleWithNoScopeOrCondition)).to.be(false); - }); - - }); - - describe('buildRuleDescription', () => { - - it('returns expected rule descriptions', () => { - expect(buildRuleDescription(ruleWithCondition)).to.be( - 'skip result when actual is greater than 10'); - expect(buildRuleDescription(ruleWithScope)).to.be( - 'skip result when instance is in test_aws_instances'); - expect(buildRuleDescription(ruleWithConditionAndScope)).to.be( - 'skip result when typical is less than 100 AND instance is not in test_aws_instances'); - }); - }); - - describe('getAppliesToValueFromAnomaly', () => { - - const anomaly = { - actual: [210], - typical: [1.23], - }; - - it('returns expected actual value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).to.be(210); - }); - - it('returns expected typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).to.be(1.23); - }); - - it('returns expected diff from typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).to.be(208.77); - }); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/utils.js b/x-pack/legacy/plugins/ml/public/components/rule_editor/utils.js deleted file mode 100644 index 84f1c8207f3ff..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/rule_editor/utils.js +++ /dev/null @@ -1,334 +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 { ACTION, APPLIES_TO, FILTER_TYPE, OPERATOR } from '../../../common/constants/detector_rule'; - -import { cloneDeep } from 'lodash'; -import { ml } from '../../services/ml_api_service'; -import { mlJobService } from '../../services/job_service'; -import { i18n } from '@kbn/i18n'; -import { processCreatedBy } from '../../../common/util/job_utils'; - -export function getNewConditionDefaults() { - return { - applies_to: APPLIES_TO.ACTUAL, - operator: OPERATOR.LESS_THAN, - value: 1 - }; -} - -export function getNewRuleDefaults() { - return { - actions: [ACTION.SKIP_RESULT], - conditions: [] - }; -} - -export function getScopeFieldDefaults(filterListIds) { - const defaults = { - filter_type: FILTER_TYPE.INCLUDE, - enabled: false, // UI-only property to show field as enabled in Scope section. - }; - - if (filterListIds !== undefined && filterListIds.length > 0) { - defaults.filter_id = filterListIds[0]; - } - - return defaults; -} - -export function isValidRule(rule) { - // Runs simple checks to make sure the minimum set of - // properties have values in the edited rule. - let isValid = false; - - // Check an action has been supplied. - const actions = rule.actions; - if (actions.length > 0) { - // Check either a condition or a scope property has been set. - const conditions = rule.conditions; - if (conditions !== undefined && conditions.length > 0) { - isValid = true; - } else { - const scope = rule.scope; - if (scope !== undefined) { - isValid = Object.keys(scope).some(field => (scope[field].enabled === true)); - } - } - } - - return isValid; -} - -export function saveJobRule(job, detectorIndex, ruleIndex, editedRule) { - const detector = job.analysis_config.detectors[detectorIndex]; - - // Filter out any scope expression where the UI=specific 'enabled' - // property is set to false. - const clonedRule = cloneDeep(editedRule); - const scope = clonedRule.scope; - if (scope !== undefined) { - Object.keys(scope).forEach((field) => { - if (scope[field].enabled === false) { - delete scope[field]; - } else { - // Remove the UI-only property as it is rejected by the endpoint. - delete scope[field].enabled; - } - }); - } - - let rules = []; - if (detector.custom_rules === undefined) { - rules = [clonedRule]; - } else { - rules = cloneDeep(detector.custom_rules); - - if (ruleIndex < rules.length) { - // Edit to an existing rule. - rules[ruleIndex] = clonedRule; - } else { - // Add a new rule. - rules.push(clonedRule); - } - } - - return updateJobRules(job, detectorIndex, rules); -} - -export function deleteJobRule(job, detectorIndex, ruleIndex) { - const detector = job.analysis_config.detectors[detectorIndex]; - let customRules = []; - if (detector.custom_rules !== undefined && ruleIndex < detector.custom_rules.length) { - customRules = cloneDeep(detector.custom_rules); - customRules.splice(ruleIndex, 1); - return updateJobRules(job, detectorIndex, customRules); - } else { - return Promise.reject(new Error( - i18n.translate('xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage', { - defaultMessage: 'Rule no longer exists for detector index {detectorIndex} in job {jobId}', - values: { - detectorIndex, - jobId: job.job_id - } - }) - )); - } -} - - -export function updateJobRules(job, detectorIndex, rules) { - // Pass just the detector with the edited rule to the updateJob endpoint. - const jobId = job.job_id; - const jobData = { - detectors: [ - { - detector_index: detectorIndex, - custom_rules: rules - } - ] - }; - - let customSettings = {}; - if (job.custom_settings !== undefined) { - customSettings = { ...job.custom_settings }; - processCreatedBy(customSettings); - jobData.custom_settings = customSettings; - } - - return new Promise((resolve, reject) => { - mlJobService.updateJob(jobId, jobData) - .then((resp) => { - if (resp.success) { - // Refresh the job data in the job service before resolving. - mlJobService.refreshJob(jobId) - .then(() => { - resolve({ success: true }); - }) - .catch((refreshResp) => { - reject(refreshResp); - }); - } else { - reject(resp); - } - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Updates an ML filter used in the scope part of a rule, -// adding an item to the filter with the specified ID. -export function addItemToFilter(item, filterId) { - return new Promise((resolve, reject) => { - ml.filters.updateFilter( - filterId, - undefined, - [item], - undefined - ) - .then((updatedFilter) => { - resolve(updatedFilter); - }) - .catch((error) => { - reject(error); - }); - }); -} - -export function buildRuleDescription(rule) { - const { actions, conditions, scope } = rule; - let actionsText = ''; - let conditionsText = ''; - let filtersText = ''; - - actions.forEach((action, i) => { - if (i > 0) { - actionsText += ' AND '; - } - switch (action) { - case ACTION.SKIP_RESULT: - actionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.resultActionTypeText', { - defaultMessage: 'result', - description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText +' + - 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' - }); - break; - case ACTION.SKIP_MODEL_UPDATE: - actionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.modelUpdateActionTypeText', { - defaultMessage: 'model update', - description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + - 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' - }); - break; - } - }); - - if (conditions !== undefined) { - conditions.forEach((condition, i) => { - if (i > 0) { - conditionsText += ' AND '; - } - conditionsText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.conditionsText', { - defaultMessage: '{appliesTo} is {operator} {value}', - values: { appliesTo: appliesToText(condition.applies_to), operator: operatorToText(condition.operator), value: condition.value }, - description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + - 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' - }); - }); - } - - if (scope !== undefined) { - if (conditions !== undefined && conditions.length > 0) { - filtersText += ' AND '; - } - const fieldNames = Object.keys(scope); - fieldNames.forEach((fieldName, i) => { - if (i > 0) { - filtersText += ' AND '; - } - - const filter = scope[fieldName]; - filtersText += i18n.translate('xpack.ml.ruleEditor.ruleDescription.filtersText', { - defaultMessage: '{fieldName} is {filterType} {filterId}', - values: { fieldName, filterType: filterTypeToText(filter.filter_type), filterId: filter.filter_id }, - description: 'Part of composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + - 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText' - }); - }); - } - - return i18n.translate('xpack.ml.ruleEditor.ruleDescription', { - defaultMessage: 'skip {actions} when {conditions}{filters}', - values: { - actions: actionsText, - conditions: conditionsText, - filters: filtersText - }, - description: 'Composite text: xpack.ml.ruleEditor.ruleDescription.[actionName]ActionTypeText + ' + - 'xpack.ml.ruleEditor.ruleDescription.conditionsText + xpack.ml.ruleEditor.ruleDescription.filtersText.' + - ' (Example: skip model update when actual is less than 1 AND ip is in xxx)' - }); -} - -export function filterTypeToText(filterType) { - switch (filterType) { - case FILTER_TYPE.INCLUDE: - return i18n.translate('xpack.ml.ruleEditor.includeFilterTypeText', { defaultMessage: 'in' }); - case FILTER_TYPE.EXCLUDE: - return i18n.translate('xpack.ml.ruleEditor.excludeFilterTypeText', { defaultMessage: 'not in' }); - - default: - return (filterType !== undefined) ? filterType : ''; - } -} - -export function appliesToText(appliesTo) { - switch (appliesTo) { - case APPLIES_TO.ACTUAL: - return i18n.translate('xpack.ml.ruleEditor.actualAppliesTypeText', { defaultMessage: 'actual' }); - case APPLIES_TO.TYPICAL: - return i18n.translate('xpack.ml.ruleEditor.typicalAppliesTypeText', { defaultMessage: 'typical' }); - - case APPLIES_TO.DIFF_FROM_TYPICAL: - return i18n.translate('xpack.ml.ruleEditor.diffFromTypicalAppliesTypeText', { defaultMessage: 'diff from typical' }); - - default: - return (appliesTo !== undefined) ? appliesTo : ''; - } -} - -export function operatorToText(operator) { - switch (operator) { - case OPERATOR.LESS_THAN: - return i18n.translate('xpack.ml.ruleEditor.lessThanOperatorTypeText', { defaultMessage: 'less than' }); - - case OPERATOR.LESS_THAN_OR_EQUAL: - return i18n.translate('xpack.ml.ruleEditor.lessThanOrEqualToOperatorTypeText', { defaultMessage: 'less than or equal to' }); - - case OPERATOR.GREATER_THAN: - return i18n.translate('xpack.ml.ruleEditor.greaterThanOperatorTypeText', { defaultMessage: 'greater than' }); - - case OPERATOR.GREATER_THAN_OR_EQUAL: - return i18n.translate('xpack.ml.ruleEditor.greaterThanOrEqualToOperatorTypeText', { defaultMessage: 'greater than or equal to' }); - - default: - return (operator !== undefined) ? operator : ''; - } -} - -// Returns the value of the selected 'applies_to' field from the -// selected anomaly i.e. the actual, typical or diff from typical. -export function getAppliesToValueFromAnomaly(anomaly, appliesTo) { - let actualValue; - let typicalValue; - - const actual = anomaly.actual; - if (actual !== undefined) { - actualValue = Array.isArray(actual) ? actual[0] : actual; - } - - const typical = anomaly.typical; - if (typical !== undefined) { - typicalValue = Array.isArray(typical) ? typical[0] : typical; - } - - switch (appliesTo) { - case APPLIES_TO.ACTUAL: - return actualValue; - - case APPLIES_TO.TYPICAL: - return typicalValue; - - case APPLIES_TO.DIFF_FROM_TYPICAL: - if (actual !== undefined && typical !== undefined) { - return Math.abs(actualValue - typicalValue); - } - } - - return undefined; -} diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/saved_search.ts deleted file mode 100644 index 07979d7c1bd11..0000000000000 --- a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/saved_search.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { searchSourceMock } from '../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; - -export const savedSearchMock = { - id: 'the-saved-search-id', - title: 'the-saved-search-title', - searchSource: searchSourceMock, - columns: [], - sort: [], - destroy: () => {}, -}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss deleted file mode 100644 index 4c0ecd8f9ce44..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import 'pages/analytics_exploration/components/exploration/index'; -@import 'pages/analytics_management/components/analytics_list/index'; -@import 'pages/analytics_management/components/create_analytics_form/index'; -@import 'pages/analytics_management/components/create_analytics_flyout/index'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/breadcrumbs.ts deleted file mode 100644 index 23de4c5b69ac7..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/breadcrumbs.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { ML_BREADCRUMB } from '../breadcrumbs'; - -export function getDataFrameAnalyticsBreadcrumbs() { - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts deleted file mode 100644 index 112f828f9897e..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { - getAnalysisType, - getDependentVar, - getPredictionFieldName, - isOutlierAnalysis, - refreshAnalyticsList$, - useRefreshAnalyticsList, - DataFrameAnalyticsId, - DataFrameAnalyticsConfig, - IndexName, - IndexPattern, - REFRESH_ANALYTICS_LIST_STATE, - ANALYSIS_CONFIG_TYPE, - RegressionEvaluateResponse, - getValuesFromResponse, - loadEvalData, - Eval, - getPredictedFieldName, - INDEX_STATUS, - SEARCH_SIZE, -} from './analytics'; - -export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getFlattenedFields, - sortColumns, - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - EsId, - EsDoc, - EsDocSource, - EsFieldName, - MAX_COLUMNS, -} from './fields'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts deleted file mode 100644 index 2a07bc1251a31..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts +++ /dev/null @@ -1,143 +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 React, { useEffect, useState } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; - -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - SEARCH_SIZE, -} from '../../../../common'; - -import { getOutlierScoreFieldName } from './common'; - -type TableItem = Record; - -interface LoadExploreDataArg { - field: string; - direction: SortDirection; -} -export interface UseExploreDataReturnType { - errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; - status: INDEX_STATUS; - tableItems: TableItem[]; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - selectedFields: EsFieldName[], - setSelectedFields: React.Dispatch> -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - const [tableItems, setTableItems] = useState([]); - const [sortField, setSortField] = useState(''); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - - const loadExploreData = async ({ field, direction }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const resp: SearchResponse = await ml.esSearch({ - index: jobConfig.dest.index, - size: SEARCH_SIZE, - body: { - query: { match_all: {} }, - sort: [ - { - [field]: { - order: direction, - }, - }, - ], - }, - }); - - setSortField(field); - setSortDirection(direction); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - if (jobConfig !== undefined) { - loadExploreData({ - field: getOutlierScoreFieldName(jobConfig), - direction: SORT_DIRECTION.DESC, - }); - } - }, [jobConfig && jobConfig.id]); - - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; -}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx deleted file mode 100644 index 20ab6678da820..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ /dev/null @@ -1,210 +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 React, { FC, Fragment, useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; -import { ErrorCallout } from './error_callout'; -import { - getValuesFromResponse, - getDependentVar, - getPredictionFieldName, - loadEvalData, - Eval, - DataFrameAnalyticsConfig, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -interface Props { - jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; -} - -const meanSquaredErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', - { - defaultMessage: 'Mean squared error', - } -); -const rSquaredText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', - { - defaultMessage: 'R squared', - } -); -const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; - -export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { - const [trainingEval, setTrainingEval] = useState(defaultEval); - const [generalizationEval, setGeneralizationEval] = useState(defaultEval); - const [isLoadingTraining, setIsLoadingTraining] = useState(false); - const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); - - const index = jobConfig.dest.index; - const dependentVariable = getDependentVar(jobConfig.analysis); - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - // default is 'ml' - const resultsField = jobConfig.dest.results_field; - - const loadData = async () => { - setIsLoadingGeneralization(true); - setIsLoadingTraining(true); - - const genErrorEval = await loadEvalData({ - isTraining: false, - index, - dependentVariable, - resultsField, - predictionFieldName, - }); - - if (genErrorEval.success === true && genErrorEval.eval) { - const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); - setGeneralizationEval({ - meanSquaredError, - rSquared, - error: null, - }); - setIsLoadingGeneralization(false); - } else { - setIsLoadingGeneralization(false); - setGeneralizationEval({ - meanSquaredError: '', - rSquared: '', - error: genErrorEval.error, - }); - } - - const trainingErrorEval = await loadEvalData({ - isTraining: true, - index, - dependentVariable, - resultsField, - predictionFieldName, - }); - - if (trainingErrorEval.success === true && trainingErrorEval.eval) { - const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); - setTrainingEval({ - meanSquaredError, - rSquared, - error: null, - }); - setIsLoadingTraining(false); - } else { - setIsLoadingTraining(false); - setTrainingEval({ - meanSquaredError: '', - rSquared: '', - error: genErrorEval.error, - }); - } - }; - - useEffect(() => { - loadData(); - }, []); - - return ( - - - - - - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { - defaultMessage: 'Regression job ID {jobId}', - values: { jobId: jobConfig.id }, - })} - - - - - {getTaskStateBadge(jobStatus)} - - - - - - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle', - { - defaultMessage: 'Generalization error', - } - )} - - - - - {generalizationEval.error !== null && } - {generalizationEval.error === null && ( - - - - - - - - - )} - - - - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle', - { - defaultMessage: 'Training error', - } - )} - - - - - {trainingEval.error !== null && } - {trainingEval.error === null && ( - - - - - - - - - )} - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index 5ba3b8ed45939..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,467 +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 React, { Fragment, FC, useEffect, useState } from 'react'; -import moment from 'moment-timezone'; - -import { i18n } from '@kbn/i18n'; -import { - EuiBadge, - EuiButtonIcon, - EuiCallOut, - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiPopover, - EuiPopoverTitle, - EuiProgress, - EuiSpacer, - EuiText, - EuiToolTip, - Query, -} from '@elastic/eui'; - -import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; - -import { - ColumnType, - MlInMemoryTableBasic, - OnTableChangeArg, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../components/ml_in_memory_table'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; - -import { - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - DataFrameAnalyticsConfig, - EsFieldName, - EsDoc, - MAX_COLUMNS, - getPredictedFieldName, - INDEX_STATUS, - SEARCH_SIZE, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -import { useExploreData, defaultSearchQuery } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -interface Props { - jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; -} - -export const ResultsTable: FC = React.memo(({ jobConfig, jobStatus }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [searchError, setSearchError] = useState(undefined); - const [searchString, setSearchString] = useState(undefined); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } - - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } - - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([...toggleSelectedField(selectedFields, column)]); - } - } - - const { - errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData(jobConfig, selectedFields, setSelectedFields); - - let docFields: EsFieldName[] = []; - let docFieldsCount = 0; - if (tableItems.length > 0) { - docFields = Object.keys(tableItems[0]); - docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); - docFieldsCount = docFields.length; - } - - const columns: ColumnType[] = []; - - if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { - columns.push( - ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { - const column: ColumnType = { - field: k, - name: k, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', - { - defaultMessage: 'array', - } - )} - - - ); - } else if (typeof d === 'object' && d !== null) { - // If the cells data is an object, display a 'object' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent', - { - defaultMessage: 'object', - } - )} - - - ); - } - - return d; - }; - - let columnType; - - if (tableItems.length > 0) { - columnType = typeof tableItems[0][k]; - } - - if (typeof columnType !== 'undefined') { - switch (columnType) { - case 'boolean': - column.dataType = 'boolean'; - break; - case 'Date': - column.align = 'right'; - column.render = (d: any) => - formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case 'number': - column.dataType = 'number'; - column.render = render; - break; - default: - column.render = render; - break; - } - } else { - column.render = render; - } - - return column; - }) - ); - } - - useEffect(() => { - if (jobConfig !== undefined) { - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const predictedFieldSelected = selectedFields.includes(predictedFieldName); - - const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); - } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // by default set the sorting to descending on the prediction field (`_prediction`). - // if that's not available sort ascending on the first column. - // also check if the current sorting field is still available. - if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const predictedFieldSelected = selectedFields.includes(predictedFieldName); - - const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); - } - }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); - - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - if (sort.field !== sortField || sort.direction !== sortDirection) { - loadExploreData({ ...sort, searchQuery }); - } - }; - } - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - } catch (e) { - setSearchError(e.toString()); - } - } - }; - - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, - { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], - }, - ], - }; - - if (jobConfig === undefined) { - return null; - } - - if (status === INDEX_STATUS.ERROR) { - return ( - - - - - - - {getTaskStateBadge(jobStatus)} - - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - - {getTaskStateBadge(jobStatus)} - - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - - )} - - - - - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - -
- {docFields.map(d => ( - toggleColumn(d)} - disabled={selectedFields.includes(d) && selectedFields.length === 1} - /> - ))} -
-
-
-
-
-
-
- {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - )} -
- ); -}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index 3e7266eb89474..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,163 +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. - */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { useEffect, useState } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; - -import { - getDefaultRegressionFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - getPredictedFieldName, - INDEX_STATUS, - SEARCH_SIZE, -} from '../../../../common'; - -export const defaultSearchQuery = { - match_all: {}, -}; - -type TableItem = Record; - -interface LoadExploreDataArg { - field: string; - direction: SortDirection; - searchQuery: SavedSearchQuery; -} -export interface UseExploreDataReturnType { - errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; - status: INDEX_STATUS; - tableItems: TableItem[]; -} - -interface SearchQuery { - query: SavedSearchQuery; - sort?: any; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - selectedFields: EsFieldName[], - setSelectedFields: React.Dispatch> -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - const [tableItems, setTableItems] = useState([]); - const [sortField, setSortField] = useState(''); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - - const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const body: SearchQuery = { - query: searchQuery, - }; - - if (field !== undefined) { - body.sort = [ - { - [field]: { - order: direction, - }, - }, - ]; - } - - const resp: SearchResponse = await ml.esSearch({ - index: jobConfig.dest.index, - size: SEARCH_SIZE, - body, - }); - - setSortField(field); - setSortDirection(direction); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultRegressionFields(docs, jobConfig); - setSelectedFields(newSelectedFields); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - if (jobConfig !== undefined) { - loadExploreData({ - field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis), - direction: SORT_DIRECTION.DESC, - searchQuery: defaultSearchQuery, - }); - } - }, [jobConfig && jobConfig.id]); - - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; -}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx deleted file mode 100644 index 76df839b9346b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx +++ /dev/null @@ -1,69 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { IndexPatterns } from 'ui/index_patterns'; -import { I18nContext } from 'ui/i18n'; - -import { InjectorService } from '../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsExploration', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const globalState = $injector.get('globalState'); - globalState.fetch(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx deleted file mode 100644 index 63ddedaf65689..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx +++ /dev/null @@ -1,61 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { IndexPatterns } from 'ui/index_patterns'; -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsManagement', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts deleted file mode 100644 index f0fa2ad3b66db..0000000000000 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ /dev/null @@ -1,336 +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 { idx } from '@kbn/elastic-idx'; -import { i18n } from '@kbn/i18n'; - -import { validateIndexPattern } from 'ui/index_patterns'; - -import { isValidIndexName } from '../../../../../../common/util/es_utils'; - -import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State, JOB_TYPES } from './state'; -import { - isJobIdValid, - validateModelMemoryLimitUnits, -} from '../../../../../../common/util/job_utils'; -import { maxLengthValidator } from '../../../../../../common/util/validators'; -import { - JOB_ID_MAX_LENGTH, - ALLOWED_DATA_UNITS, -} from '../../../../../../common/constants/validation'; -import { getDependentVar, isRegressionAnalysis } from '../../../../common/analytics'; - -const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( - ', ' -)} or ${[...ALLOWED_DATA_UNITS].pop()}`; - -export const mmlUnitInvalidErrorMessage = i18n.translate( - 'xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', - { - defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}', - values: { str: mmlAllowedUnitsStr }, - } -); - -const getSourceIndexString = (state: State) => { - const { jobConfig } = state; - - const sourceIndex = idx(jobConfig, _ => _.source.index); - - if (typeof sourceIndex === 'string') { - return sourceIndex; - } - - if (Array.isArray(sourceIndex)) { - return sourceIndex.join(','); - } - - return ''; -}; - -export const validateAdvancedEditor = (state: State): State => { - const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form; - const { jobConfig } = state; - - state.advancedEditorMessages = []; - - const sourceIndexName = getSourceIndexString(state); - const sourceIndexNameEmpty = sourceIndexName === ''; - // general check against Kibana index pattern names, but since this is about the advanced editor - // with support for arrays in the job config, we also need to check that each individual name - // doesn't include a comma if index names are supplied as an array. - // `validateIndexPattern()` returns a map of messages, we're only interested here if it's valid or not. - // If there are no messages, it means the index pattern is valid. - let sourceIndexNameValid = Object.keys(validateIndexPattern(sourceIndexName)).length === 0; - const sourceIndex = idx(jobConfig, _ => _.source.index); - if (sourceIndexNameValid) { - if (typeof sourceIndex === 'string') { - sourceIndexNameValid = !sourceIndex.includes(','); - } - if (Array.isArray(sourceIndex)) { - sourceIndexNameValid = !sourceIndex.some(d => d.includes(',')); - } - } - - const destinationIndexName = idx(jobConfig, _ => _.dest.index) || ''; - const destinationIndexNameEmpty = destinationIndexName === ''; - const destinationIndexNameValid = isValidIndexName(destinationIndexName); - const destinationIndexPatternTitleExists = - state.indexPatternsMap[destinationIndexName] !== undefined; - const mml = jobConfig.model_memory_limit; - const modelMemoryLimitEmpty = mml === ''; - if (!modelMemoryLimitEmpty && mml !== undefined) { - const { valid } = validateModelMemoryLimitUnits(mml); - state.form.modelMemoryLimitUnitValid = valid; - } - - let dependentVariableEmpty = false; - if (isRegressionAnalysis(jobConfig.analysis)) { - const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; - dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariableName === ''; - } - - if (sourceIndexNameEmpty) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty', - { - defaultMessage: 'The source index name must not be empty.', - } - ), - message: '', - }); - } else if (!sourceIndexNameValid) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameValid', - { - defaultMessage: 'Invalid source index name.', - } - ), - message: '', - }); - } - - if (destinationIndexNameEmpty) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty', - { - defaultMessage: 'The destination index name must not be empty.', - } - ), - message: '', - }); - } else if (!destinationIndexNameValid) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid', - { - defaultMessage: 'Invalid destination index name.', - } - ), - message: '', - }); - } - - if (dependentVariableEmpty) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.dependentVariableEmpty', - { - defaultMessage: 'The dependent variable field must not be empty.', - } - ), - message: '', - }); - } - - if (modelMemoryLimitEmpty) { - state.advancedEditorMessages.push({ - error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty', - { - defaultMessage: 'The model memory limit field must not be empty.', - } - ), - message: '', - }); - } - - if (!state.form.modelMemoryLimitUnitValid) { - state.advancedEditorMessages.push({ - error: mmlUnitInvalidErrorMessage, - message: '', - }); - } - - state.isValid = - state.form.modelMemoryLimitUnitValid && - !jobIdEmpty && - jobIdValid && - !jobIdExists && - !sourceIndexNameEmpty && - sourceIndexNameValid && - !destinationIndexNameEmpty && - destinationIndexNameValid && - !dependentVariableEmpty && - !modelMemoryLimitEmpty && - (!destinationIndexPatternTitleExists || !createIndexPattern); - - return state; -}; - -const validateForm = (state: State): State => { - const { - jobIdEmpty, - jobIdValid, - jobIdExists, - jobType, - sourceIndexNameEmpty, - sourceIndexNameValid, - destinationIndexNameEmpty, - destinationIndexNameValid, - destinationIndexPatternTitleExists, - createIndexPattern, - dependentVariable, - modelMemoryLimit, - } = state.form; - - const dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariable === ''; - const modelMemoryLimitEmpty = modelMemoryLimit === ''; - - if (!modelMemoryLimitEmpty && modelMemoryLimit !== undefined) { - const { valid } = validateModelMemoryLimitUnits(modelMemoryLimit); - state.form.modelMemoryLimitUnitValid = valid; - } - - state.isValid = - state.form.modelMemoryLimitUnitValid && - !jobIdEmpty && - jobIdValid && - !jobIdExists && - !sourceIndexNameEmpty && - sourceIndexNameValid && - !destinationIndexNameEmpty && - destinationIndexNameValid && - !dependentVariableEmpty && - !modelMemoryLimitEmpty && - (!destinationIndexPatternTitleExists || !createIndexPattern); - - return state; -}; - -export function reducer(state: State, action: Action): State { - switch (action.type) { - case ACTION.ADD_REQUEST_MESSAGE: - const requestMessages = state.requestMessages; - requestMessages.push(action.requestMessage); - return { ...state, requestMessages }; - - case ACTION.RESET_REQUEST_MESSAGES: - return { ...state, requestMessages: [] }; - - case ACTION.CLOSE_MODAL: - return { ...state, isModalVisible: false }; - - case ACTION.OPEN_MODAL: - return { ...state, isModalVisible: true }; - - case ACTION.RESET_ADVANCED_EDITOR_MESSAGES: - return { ...state, advancedEditorMessages: [] }; - - case ACTION.RESET_FORM: - return getInitialState(); - - case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: - return { ...state, advancedEditorRawString: action.advancedEditorRawString }; - - case ACTION.SET_FORM_STATE: - const newFormState = { ...state.form, ...action.payload }; - - // update state attributes which are derived from other state attributes. - if (action.payload.destinationIndex !== undefined) { - newFormState.destinationIndexNameExists = state.indexNames.some( - name => newFormState.destinationIndex === name - ); - newFormState.destinationIndexNameEmpty = newFormState.destinationIndex === ''; - newFormState.destinationIndexNameValid = isValidIndexName(newFormState.destinationIndex); - newFormState.destinationIndexPatternTitleExists = - state.indexPatternsMap[newFormState.destinationIndex] !== undefined; - } - - if (action.payload.jobId !== undefined) { - newFormState.jobIdExists = state.jobIds.some(id => newFormState.jobId === id); - newFormState.jobIdEmpty = newFormState.jobId === ''; - newFormState.jobIdValid = isJobIdValid(newFormState.jobId); - newFormState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)( - newFormState.jobId - ); - } - - if (action.payload.sourceIndex !== undefined) { - newFormState.sourceIndexNameEmpty = newFormState.sourceIndex === ''; - const validationMessages = validateIndexPattern(newFormState.sourceIndex); - newFormState.sourceIndexNameValid = Object.keys(validationMessages).length === 0; - } - - return state.isAdvancedEditorEnabled - ? validateAdvancedEditor({ ...state, form: newFormState }) - : validateForm({ ...state, form: newFormState }); - - case ACTION.SET_INDEX_NAMES: { - const newState = { ...state, indexNames: action.indexNames }; - newState.form.destinationIndexNameExists = newState.indexNames.some( - name => newState.form.destinationIndex === name - ); - return newState; - } - - case ACTION.SET_INDEX_PATTERN_TITLES: { - const newState = { - ...state, - ...action.payload, - }; - newState.form.destinationIndexPatternTitleExists = - newState.indexPatternsMap[newState.form.destinationIndex] !== undefined; - return newState; - } - - case ACTION.SET_IS_JOB_CREATED: - return { ...state, isJobCreated: action.isJobCreated }; - - case ACTION.SET_IS_JOB_STARTED: - return { ...state, isJobStarted: action.isJobStarted }; - - case ACTION.SET_IS_MODAL_BUTTON_DISABLED: - return { ...state, isModalButtonDisabled: action.isModalButtonDisabled }; - - case ACTION.SET_IS_MODAL_VISIBLE: - return { ...state, isModalVisible: action.isModalVisible }; - - case ACTION.SET_JOB_CONFIG: - return validateAdvancedEditor({ ...state, jobConfig: action.payload }); - - case ACTION.SET_JOB_IDS: { - const newState = { ...state, jobIds: action.jobIds }; - newState.form.jobIdExists = newState.jobIds.some(id => newState.form.jobId === id); - return newState; - } - - case ACTION.SWITCH_TO_ADVANCED_EDITOR: - const jobConfig = getJobConfigFromFormState(state.form); - return validateAdvancedEditor({ - ...state, - advancedEditorRawString: JSON.stringify(jobConfig, null, 2), - isAdvancedEditorEnabled: true, - jobConfig, - }); - } - - return state; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/breadcrumbs.ts deleted file mode 100644 index 5534b37a6e427..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/breadcrumbs.ts +++ /dev/null @@ -1,13 +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 { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/breadcrumbs.ts deleted file mode 100644 index 3e3f7e986b3d7..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/breadcrumbs.ts +++ /dev/null @@ -1,23 +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 { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; - -export function getFileDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/breadcrumbs.ts deleted file mode 100644 index 03c66335ddb6c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/breadcrumbs.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 { i18n } from '@kbn/i18n'; -import { - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - // @ts-ignore -} from '../../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/request.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/request.ts deleted file mode 100644 index 83acda0419a0c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/request.ts +++ /dev/null @@ -1,13 +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 { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; - -export interface FieldRequestConfig { - fieldName?: string; - type: ML_JOB_FIELD_TYPES; - cardinality: number; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx deleted file mode 100644 index df152b80c315e..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx +++ /dev/null @@ -1,61 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { IndexPatterns } from 'ui/index_patterns'; -import { InjectorService } from '../../../common/types/angular'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; -import { createSearchItems } from '../../jobs/new_job/utils/new_job_utils'; - -import { Page } from './page'; - -module.directive('mlDataVisualizer', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx deleted file mode 100644 index cf07fdf0ab2ca..0000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx +++ /dev/null @@ -1,676 +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 React, { FC, Fragment, useEffect, useState } from 'react'; -import { merge } from 'rxjs'; -import { i18n } from '@kbn/i18n'; - -import { FieldType } from 'ui/index_patterns'; -import { timefilter } from 'ui/timefilter'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPage, - EuiPageBody, - EuiPageContentBody, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { KBN_FIELD_TYPES, esQuery } from '../../../../../../../src/plugins/data/public'; -import { NavigationMenu } from '../../components/navigation_menu'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; -import { isFullLicense } from '../../license/check_license'; -import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; -import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; -import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; -import { kbnTypeToMLJobType } from '../../util/field_types_utils'; -import { timeBasedIndexCheck } from '../../util/index_utils'; -import { TimeBuckets } from '../../util/time_buckets'; -import { FieldRequestConfig, FieldVisConfig } from './common'; -import { ActionsPanel } from './components/actions_panel'; -import { FieldsPanel } from './components/fields_panel'; -import { SearchPanel } from './components/search_panel'; -import { DataLoader } from './data_loader'; - -interface DataVisualizerPageState { - searchQuery: string | SavedSearchQuery; - searchString: string | SavedSearchQuery; - searchQueryLanguage: SEARCH_QUERY_LANGUAGE; - samplerShardSize: number; - overallStats: any; - metricConfigs: FieldVisConfig[]; - totalMetricFieldCount: number; - populatedMetricFieldCount: number; - showAllMetrics: boolean; - metricFieldQuery?: string; - nonMetricConfigs: FieldVisConfig[]; - totalNonMetricFieldCount: number; - populatedNonMetricFieldCount: number; - showAllNonMetrics: boolean; - nonMetricShowFieldType: ML_JOB_FIELD_TYPES | '*'; - nonMetricFieldQuery?: string; -} - -const defaultSearchQuery = { - match_all: {}, -}; - -function getDefaultPageState(): DataVisualizerPageState { - return { - searchString: '', - searchQuery: defaultSearchQuery, - searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, - samplerShardSize: 5000, - overallStats: { - totalCount: 0, - aggregatableExistsFields: [], - aggregatableNotExistsFields: [], - nonAggregatableExistsFields: [], - nonAggregatableNotExistsFields: [], - }, - metricConfigs: [], - totalMetricFieldCount: 0, - populatedMetricFieldCount: 0, - showAllMetrics: false, - nonMetricConfigs: [], - totalNonMetricFieldCount: 0, - populatedNonMetricFieldCount: 0, - showAllNonMetrics: false, - nonMetricShowFieldType: '*', - }; -} - -export const Page: FC = () => { - const kibanaContext = useKibanaContext(); - - const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = kibanaContext; - - const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); - - useEffect(() => { - if (currentIndexPattern.timeFieldName !== undefined) { - timefilter.enableTimeRangeSelector(); - } else { - timefilter.disableTimeRangeSelector(); - } - - timefilter.enableAutoRefreshSelector(); - timeBasedIndexCheck(currentIndexPattern, true); - }, []); - - // Obtain the list of non metric field types which appear in the index pattern. - let indexedFieldTypes: ML_JOB_FIELD_TYPES[] = []; - const indexPatternFields: FieldType[] = currentIndexPattern.fields; - indexPatternFields.forEach(field => { - if (field.scripted !== true) { - const dataVisualizerType: ML_JOB_FIELD_TYPES | undefined = kbnTypeToMLJobType(field); - if ( - dataVisualizerType !== undefined && - !indexedFieldTypes.includes(dataVisualizerType) && - dataVisualizerType !== ML_JOB_FIELD_TYPES.NUMBER - ) { - indexedFieldTypes.push(dataVisualizerType); - } - } - }); - indexedFieldTypes = indexedFieldTypes.sort(); - - const defaults = getDefaultPageState(); - - const [showActionsPanel] = useState( - isFullLicense() && currentIndexPattern.timeFieldName !== undefined - ); - - const [searchString, setSearchString] = useState(defaults.searchString); - const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); - const [searchQueryLanguage, setSearchQueryLanguage] = useState(defaults.searchQueryLanguage); - const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); - - // TODO - type overallStats and stats - const [overallStats, setOverallStats] = useState(defaults.overallStats); - - const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); - const [totalMetricFieldCount, setTotalMetricFieldCount] = useState( - defaults.totalMetricFieldCount - ); - const [populatedMetricFieldCount, setPopulatedMetricFieldCount] = useState( - defaults.populatedMetricFieldCount - ); - const [showAllMetrics, setShowAllMetrics] = useState(defaults.showAllMetrics); - const [metricFieldQuery, setMetricFieldQuery] = useState(defaults.metricFieldQuery); - - const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); - const [totalNonMetricFieldCount, setTotalNonMetricFieldCount] = useState( - defaults.totalNonMetricFieldCount - ); - const [populatedNonMetricFieldCount, setPopulatedNonMetricFieldCount] = useState( - defaults.populatedNonMetricFieldCount - ); - const [showAllNonMetrics, setShowAllNonMetrics] = useState(defaults.showAllNonMetrics); - - const [nonMetricShowFieldType, setNonMetricShowFieldType] = useState( - defaults.nonMetricShowFieldType - ); - - const [nonMetricFieldQuery, setNonMetricFieldQuery] = useState(defaults.nonMetricFieldQuery); - - useEffect(() => { - const timeUpdateSubscription = merge( - timefilter.getTimeUpdate$(), - mlTimefilterRefresh$ - ).subscribe(loadOverallStats); - return () => { - timeUpdateSubscription.unsubscribe(); - }; - }); - - useEffect(() => { - // Check for a saved search being passed in. - const searchSource = currentSavedSearch.searchSource; - const query = searchSource.getField('query'); - if (query !== undefined) { - const queryLanguage = query.language; - const qryString = query.query; - let qry; - if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { - qry = { - query_string: { - query: qryString, - default_operator: 'AND', - }, - }; - } else { - qry = esQuery.luceneStringToDsl(qryString); - esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); - } - - setSearchQuery(qry); - setSearchString(qryString); - setSearchQueryLanguage(queryLanguage); - } - }, []); - - useEffect(() => { - loadOverallStats(); - }, [searchQuery, samplerShardSize]); - - useEffect(() => { - createMetricCards(); - createNonMetricCards(); - }, [overallStats]); - - useEffect(() => { - loadMetricFieldStats(); - }, [metricConfigs]); - - useEffect(() => { - loadNonMetricFieldStats(); - }, [nonMetricConfigs]); - - useEffect(() => { - createMetricCards(); - }, [showAllMetrics, metricFieldQuery]); - - useEffect(() => { - createNonMetricCards(); - }, [showAllNonMetrics, nonMetricShowFieldType, nonMetricFieldQuery]); - - async function loadOverallStats() { - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const allStats = await dataLoader.loadOverallData( - searchQuery, - samplerShardSize, - earliest, - latest - ); - setOverallStats(allStats); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadMetricFieldStats() { - // Only request data for fields that exist in documents. - if (metricConfigs.length === 0) { - return; - } - - const configsToLoad = metricConfigs.filter( - config => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - // Obtain the interval to use for date histogram aggregations - // (such as the document count chart). Aim for 75 bars. - const buckets = new TimeBuckets(); - - const tf = timefilter as any; - let earliest: number | undefined; - let latest: number | undefined; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); - const aggInterval = buckets.getInterval(); - - try { - const metricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existMetricFields, - aggInterval.expression - ); - - // Add the metric stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - metricConfigs.forEach(config => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } else { - // Document count card. - configWithStats.stats = metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === undefined - ); - - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadNonMetricFieldStats() { - // Only request data for fields that exist in documents. - if (nonMetricConfigs.length === 0) { - return; - } - - const configsToLoad = nonMetricConfigs.filter( - config => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const nonMetricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existNonMetricFields - ); - - // Add the field stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - nonMetricConfigs.forEach(config => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...nonMetricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setNonMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - function createMetricCards() { - const configs: FieldVisConfig[] = []; - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - - let allMetricFields = indexPatternFields.filter(f => { - return ( - f.type === KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - if (metricFieldQuery !== undefined) { - const metricFieldRegexp = new RegExp(`(${metricFieldQuery})`, 'gi'); - allMetricFields = allMetricFields.filter(f => { - const addField = f.displayName !== undefined && !!f.displayName.match(metricFieldRegexp); - return addField; - }); - } - - const metricExistsFields = allMetricFields.filter(f => { - return aggregatableExistsFields.find(existsF => { - return existsF.fieldName === f.displayName; - }); - }); - - // Add a config for 'document count', identified by no field name if indexpattern is time based. - let allFieldCount = allMetricFields.length; - let popFieldCount = metricExistsFields.length; - if (currentIndexPattern.timeFieldName !== undefined) { - configs.push({ - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - loading: true, - aggregatable: true, - }); - allFieldCount++; - popFieldCount++; - } - - // Add on 1 for the document count card. - setTotalMetricFieldCount(allFieldCount); - setPopulatedMetricFieldCount(popFieldCount); - - if (allMetricFields.length === metricExistsFields.length && showAllMetrics === false) { - setShowAllMetrics(true); - return; - } - - let aggregatableFields: any[] = overallStats.aggregatableExistsFields; - if (allMetricFields.length !== metricExistsFields.length && showAllMetrics === true) { - aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); - } - - const metricFieldsToShow = showAllMetrics === true ? allMetricFields : metricExistsFields; - - metricFieldsToShow.forEach(field => { - const fieldData = aggregatableFields.find(f => { - return f.fieldName === field.displayName; - }); - - if (fieldData !== undefined) { - const metricConfig: FieldVisConfig = { - ...fieldData, - fieldFormat: field.format, - type: ML_JOB_FIELD_TYPES.NUMBER, - loading: true, - aggregatable: true, - }; - - configs.push(metricConfig); - } - }); - - setMetricConfigs(configs); - } - - function createNonMetricCards() { - let allNonMetricFields = []; - if (nonMetricShowFieldType === '*') { - allNonMetricFields = indexPatternFields.filter(f => { - return ( - f.type !== KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - } else { - if ( - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.TEXT || - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD - ) { - const aggregatableCheck = - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD ? true : false; - allNonMetricFields = indexPatternFields.filter(f => { - return ( - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true && - f.type === KBN_FIELD_TYPES.STRING && - f.aggregatable === aggregatableCheck - ); - }); - } else { - allNonMetricFields = indexPatternFields.filter(f => { - return ( - f.type === nonMetricShowFieldType && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - } - } - - // If a field filter has been entered, perform another filter on the entered regexp. - if (nonMetricFieldQuery !== undefined) { - const nonMetricFieldRegexp = new RegExp(`(${nonMetricFieldQuery})`, 'gi'); - allNonMetricFields = allNonMetricFields.filter( - f => f.displayName !== undefined && f.displayName.match(nonMetricFieldRegexp) - ); - } - - // Obtain the list of all non-metric fields which appear in documents - // (aggregatable or not aggregatable). - const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. - let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; - - allNonMetricFields.forEach(f => { - const checkAggregatableField = aggregatableExistsFields.find( - existsField => existsField.fieldName === f.displayName - ); - - if (checkAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkAggregatableField); - } else { - const checkNonAggregatableField = nonAggregatableExistsFields.find( - existsField => existsField.fieldName === f.displayName - ); - - if (checkNonAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkNonAggregatableField); - } - } - }); - - setTotalNonMetricFieldCount(allNonMetricFields.length); - setPopulatedNonMetricFieldCount(nonMetricFieldData.length); - - if (allNonMetricFields.length === nonMetricFieldData.length && showAllNonMetrics === false) { - setShowAllNonMetrics(true); - return; - } - - if (allNonMetricFields.length !== nonMetricFieldData.length && showAllNonMetrics === true) { - // Combine the field data obtained from Elasticsearch into a single array. - nonMetricFieldData = nonMetricFieldData.concat( - overallStats.aggregatableNotExistsFields, - overallStats.nonAggregatableNotExistsFields - ); - } - - const nonMetricFieldsToShow = - showAllNonMetrics === true ? allNonMetricFields : populatedNonMetricFields; - - const configs: FieldVisConfig[] = []; - - nonMetricFieldsToShow.forEach(field => { - const fieldData = nonMetricFieldData.find(f => f.fieldName === field.displayName); - - const nonMetricConfig = { - ...fieldData, - fieldFormat: field.format, - aggregatable: field.aggregatable, - scripted: field.scripted, - loading: fieldData.existsInDocs, - }; - - // Map the field type from the Kibana index pattern to the field type - // used in the data visualizer. - const dataVisualizerType = kbnTypeToMLJobType(field); - if (dataVisualizerType !== undefined) { - nonMetricConfig.type = dataVisualizerType; - } else { - // Add a flag to indicate that this is one of the 'other' Kibana - // field types that do not yet have a specific card type. - nonMetricConfig.type = field.type; - nonMetricConfig.isUnsupportedType = true; - } - - configs.push(nonMetricConfig); - }); - - setNonMetricConfigs(configs); - } - - return ( - - - - - - - -

{currentIndexPattern.title}

-
-
- {currentIndexPattern.timeFieldName !== undefined && ( - - - - )} -
- - - - - - - - - {totalMetricFieldCount > 0 && ( - - - - - )} - - - - - {showActionsPanel === true && ( - - - - )} - - -
-
-
- ); -}; diff --git a/x-pack/legacy/plugins/ml/public/explorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/explorer/breadcrumbs.js deleted file mode 100644 index 9f70505fc8dca..0000000000000 --- a/x-pack/legacy/plugins/ml/public/explorer/breadcrumbs.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; -import { i18n } from '@kbn/i18n'; - - -export function getAnomalyExplorerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer' - }), - href: '' - } - ]; -} - diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/index.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/index.js deleted file mode 100644 index 46e1b19edfed2..0000000000000 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/index.js +++ /dev/null @@ -1,7 +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 'plugins/ml/components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/explorer/index.js b/x-pack/legacy/plugins/ml/public/explorer/index.js deleted file mode 100644 index ba21c46308360..0000000000000 --- a/x-pack/legacy/plugins/ml/public/explorer/index.js +++ /dev/null @@ -1,14 +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 'plugins/ml/explorer/explorer_controller'; -import 'plugins/ml/explorer/explorer_dashboard_service'; -import 'plugins/ml/explorer/explorer_react_wrapper_directive'; -import 'plugins/ml/explorer/explorer_charts'; -import 'plugins/ml/explorer/select_limit'; -import 'plugins/ml/components/job_selector'; diff --git a/x-pack/legacy/plugins/ml/public/formatters/__tests__/abbreviate_whole_number.js b/x-pack/legacy/plugins/ml/public/formatters/__tests__/abbreviate_whole_number.js deleted file mode 100644 index 4e85bae4b1862..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/__tests__/abbreviate_whole_number.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import expect from '@kbn/expect'; -import { abbreviateWholeNumber } from '../abbreviate_whole_number'; - -describe('ML - abbreviateWholeNumber formatter', () => { - - it('returns the correct format using default max digits', () => { - expect(abbreviateWholeNumber(1)).to.be(1); - expect(abbreviateWholeNumber(12)).to.be(12); - expect(abbreviateWholeNumber(123)).to.be(123); - expect(abbreviateWholeNumber(1234)).to.be('1k'); - expect(abbreviateWholeNumber(12345)).to.be('12k'); - expect(abbreviateWholeNumber(123456)).to.be('123k'); - expect(abbreviateWholeNumber(1234567)).to.be('1m'); - expect(abbreviateWholeNumber(12345678)).to.be('12m'); - expect(abbreviateWholeNumber(123456789)).to.be('123m'); - expect(abbreviateWholeNumber(1234567890)).to.be('1b'); - expect(abbreviateWholeNumber(5555555555555.55)).to.be('6t'); - }); - - it('returns the correct format using custom max digits', () => { - expect(abbreviateWholeNumber(1, 4)).to.be(1); - expect(abbreviateWholeNumber(12, 4)).to.be(12); - expect(abbreviateWholeNumber(123, 4)).to.be(123); - expect(abbreviateWholeNumber(1234, 4)).to.be(1234); - expect(abbreviateWholeNumber(12345, 4)).to.be('12k'); - expect(abbreviateWholeNumber(123456, 6)).to.be(123456); - expect(abbreviateWholeNumber(1234567, 4)).to.be('1m'); - expect(abbreviateWholeNumber(12345678, 3)).to.be('12m'); - expect(abbreviateWholeNumber(123456789, 9)).to.be(123456789); - expect(abbreviateWholeNumber(1234567890, 3)).to.be('1b'); - expect(abbreviateWholeNumber(5555555555555.55, 5)).to.be('6t'); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/formatters/__tests__/format_value.js b/x-pack/legacy/plugins/ml/public/formatters/__tests__/format_value.js deleted file mode 100644 index ffdbdd915d3a8..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/__tests__/format_value.js +++ /dev/null @@ -1,93 +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 moment from 'moment-timezone'; -import { formatValue } from '../format_value'; - -describe('ML - formatValue formatter', () => { - const timeOfWeekRecord = { - job_id: 'gallery_time_of_week', - result_type: 'record', - probability: 0.012818, - record_score: 53.55134, - bucket_span: 900, - detector_index: 0, - timestamp: 1530155700000, - by_field_name: 'clientip', - by_field_value: '65.55.215.39', - function: 'time_of_week', - function_description: 'time' - }; - - const timeOfDayRecord = { - job_id: 'gallery_time_of_day', - result_type: 'record', - probability: 0.012818, - record_score: 97.94245, - bucket_span: 900, - detector_index: 0, - timestamp: 1517472900000, - by_field_name: 'clientip', - by_field_value: '157.56.93.83', - function: 'time_of_day', - function_description: 'time' - }; - - // Set timezone to US/Eastern for time_of_day and time_of_week tests. - beforeEach(() => { - moment.tz.setDefault('US/Eastern'); - }); - - afterEach(() => { - moment.tz.setDefault('Browser'); - }); - - // For time_of_day and time_of_week test values which are offsets in seconds - // from UTC start of week / day are formatted correctly using the test timezone. - it('correctly formats time_of_week value from numeric input', () => { - expect(formatValue(359739, 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55'); - }); - - it('correctly formats time_of_day value from numeric input', () => { - expect(formatValue(73781, 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29'); - }); - - it('correctly formats number values from numeric input', () => { - expect(formatValue(1483228800, 'mean')).to.be(1483228800); - expect(formatValue(1234.5678, 'mean')).to.be(1234.6); - expect(formatValue(0.00012345, 'mean')).to.be(0.000123); - expect(formatValue(0, 'mean')).to.be(0); - expect(formatValue(-0.12345, 'mean')).to.be(-0.123); - expect(formatValue(-1234.5678, 'mean')).to.be(-1234.6); - expect(formatValue(-100000.1, 'mean')).to.be(-100000); - }); - - it('correctly formats time_of_week value from array input', () => { - expect(formatValue([359739], 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55'); - }); - - it('correctly formats time_of_day value from array input', () => { - expect(formatValue([73781], 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29'); - }); - - it('correctly formats number values from array input', () => { - expect(formatValue([1483228800], 'mean')).to.be(1483228800); - expect(formatValue([1234.5678], 'mean')).to.be(1234.6); - expect(formatValue([0.00012345], 'mean')).to.be(0.000123); - expect(formatValue([0], 'mean')).to.be(0); - expect(formatValue([-0.12345], 'mean')).to.be(-0.123); - expect(formatValue([-1234.5678], 'mean')).to.be(-1234.6); - expect(formatValue([-100000.1], 'mean')).to.be(-100000); - }); - - it('correctly formats multi-valued array', () => { - expect(formatValue([30.3, 26.2], 'lat_long')).to.be('[30.3,26.2]'); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/formatters/__tests__/metric_change_description.js b/x-pack/legacy/plugins/ml/public/formatters/__tests__/metric_change_description.js deleted file mode 100644 index 45864ce58bf83..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/__tests__/metric_change_description.js +++ /dev/null @@ -1,33 +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 { getMetricChangeDescription } from '../metric_change_description'; - - -describe('ML - metricChangeDescription formatter', () => { - - it('returns correct icon and message if actual > typical', () => { - expect(getMetricChangeDescription(1.01, 1)).to.eql({ iconType: 'sortUp', message: 'Unusually high' }); - expect(getMetricChangeDescription(1.123, 1)).to.eql({ iconType: 'sortUp', message: '1.1x higher' }); - expect(getMetricChangeDescription(2, 1)).to.eql({ iconType: 'sortUp', message: '2x higher' }); - expect(getMetricChangeDescription(9.5, 1)).to.eql({ iconType: 'sortUp', message: '10x higher' }); - expect(getMetricChangeDescription(1000, 1)).to.eql({ iconType: 'sortUp', message: 'More than 100x higher' }); - expect(getMetricChangeDescription(1, 0)).to.eql({ iconType: 'sortUp', message: 'Unexpected non-zero value' }); - }); - - it('returns correct icon and message if actual < typical', () => { - expect(getMetricChangeDescription(1, 1.01)).to.eql({ iconType: 'sortDown', message: 'Unusually low' }); - expect(getMetricChangeDescription(1, 1.123)).to.eql({ iconType: 'sortDown', message: '1.1x lower' }); - expect(getMetricChangeDescription(1, 2)).to.eql({ iconType: 'sortDown', message: '2x lower' }); - expect(getMetricChangeDescription(1, 9.5)).to.eql({ iconType: 'sortDown', message: '10x lower' }); - expect(getMetricChangeDescription(1, 1000)).to.eql({ iconType: 'sortDown', message: 'More than 100x lower' }); - expect(getMetricChangeDescription(0, 1)).to.eql({ iconType: 'sortDown', message: 'Unexpected zero value' }); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.js b/x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.js deleted file mode 100644 index 445681be4ff81..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -/* - * Formatter to abbreviate large whole numbers with metric prefixes. - * Uses numeral.js to format numbers longer than the specified number of - * digits with metric abbreviations e.g. 12345 as 12k, or 98000000 as 98m. -*/ -import numeral from '@elastic/numeral'; - -export function abbreviateWholeNumber(value, maxDigits = 3) { - if (Math.abs(value) < Math.pow(10, maxDigits)) { - return value; - } else { - return numeral(value).format('0a'); - } -} diff --git a/x-pack/legacy/plugins/ml/public/formatters/format_value.js b/x-pack/legacy/plugins/ml/public/formatters/format_value.js deleted file mode 100644 index 48988b5561e70..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/format_value.js +++ /dev/null @@ -1,112 +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. - */ - - - -/* - * Formatter for 'typical' and 'actual' values from machine learning results. - * For detectors which use the time_of_week or time_of_day - * functions, the filter converts the raw number, which is the number of seconds since - * midnight, into a human-readable date/time format. - */ - -import moment from 'moment'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - - -const SIGFIGS_IF_ROUNDING = 3; // Number of sigfigs to use for values < 10 - -// Formats the value of an actual or typical field from a machine learning anomaly record. -// mlFunction is the 'function' field from the ML record containing what the user entered e.g. 'high_count', -// (as opposed to the 'function_description' field which holds an ML-built display hint for the function e.g. 'count'. -// If a Kibana fieldFormat is not supplied, will fall back to default -// formatting depending on the magnitude of the value. -// For time_of_day or time_of_week functions the anomaly record -// containing the timestamp of the anomaly should be supplied in -// order to correctly format the day or week offset to the time of the anomaly. -export function formatValue(value, mlFunction, fieldFormat, record) { - // actual and typical values in anomaly record results will be arrays. - // Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long), - // simply return the formatted single value. - if (Array.isArray(value)) { - if (value.length === 1) { - return formatSingleValue(value[0], mlFunction, fieldFormat, record); - } else { - // Currently only multi-value response is for lat_long detectors. - // Return with array style formatting, with items formatted as numbers, rather than - // the default String format which is set for geo_point and geo_shape fields. - const values = value.map(val => formatSingleValue(val, mlFunction, undefined, record)); - return `[${values}]`; - } - } else { - return formatSingleValue(value, mlFunction, fieldFormat, record); - } -} - -// Formats a single value according to the specified ML function. -// If a Kibana fieldFormat is not supplied, will fall back to default -// formatting depending on the magnitude of the value. -// For time_of_day or time_of_week functions the anomaly record -// containing the timestamp of the anomaly should be supplied in -// order to correctly format the day or week offset to the time of the anomaly. -function formatSingleValue(value, mlFunction, fieldFormat, record) { - if (value === undefined || value === null) { - return ''; - } - - // If the analysis function is time_of_week/day, format as day/time. - // For time_of_week / day, actual / typical is the UTC offset in seconds from the - // start of the week / day, so need to manipulate to UTC moment of the start of the week / day - // that the anomaly occurred using record timestamp if supplied, add on the offset, and finally - // revert back to configured timezone for formatting. - if (mlFunction === 'time_of_week') { - const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date()); - const i = parseInt(value); - const utcMoment = moment.utc(d).startOf('week').add(i, 's'); - return moment(utcMoment.valueOf()).format('ddd HH:mm'); - } else if (mlFunction === 'time_of_day') { - const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date()); - const i = parseInt(value); - const utcMoment = moment.utc(d).startOf('day').add(i, 's'); - return moment(utcMoment.valueOf()).format('HH:mm'); - } else { - if (fieldFormat !== undefined) { - return fieldFormat.convert(value, 'text'); - } else { - // If no Kibana FieldFormat object provided, - // format the value depending on its magnitude. - const absValue = Math.abs(value); - if (absValue >= 10000 || absValue === Math.floor(absValue)) { - // Output 0 decimal places if whole numbers or >= 10000 - if (fieldFormat !== undefined) { - return fieldFormat.convert(value, 'text'); - } else { - return Number(value.toFixed(0)); - } - - } else if (absValue >= 10) { - // Output to 1 decimal place between 10 and 10000 - return Number(value.toFixed(1)); - } - else { - // For values < 10, output to 3 significant figures - let multiple; - if (value > 0) { - multiple = Math.pow(10, SIGFIGS_IF_ROUNDING - Math.floor(Math.log(value) / Math.LN10) - 1); - } else { - multiple = Math.pow(10, SIGFIGS_IF_ROUNDING - Math.floor(Math.log(-1 * value) / Math.LN10) - 1); - } - return (Math.round(value * multiple)) / multiple; - } - } - } -} - -// TODO - remove the filter once all uses of the formatValue Angular filter have been removed. -module.filter('formatValue', () => formatValue); - diff --git a/x-pack/legacy/plugins/ml/public/formatters/metric_change_description.js b/x-pack/legacy/plugins/ml/public/formatters/metric_change_description.js deleted file mode 100644 index 06074e9a842c5..0000000000000 --- a/x-pack/legacy/plugins/ml/public/formatters/metric_change_description.js +++ /dev/null @@ -1,139 +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. - */ - - - -/* - * Produces a concise textual description of how the - * actual value compares to the typical value for an anomaly. - */ - -import { i18n } from '@kbn/i18n'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -// Returns an Object containing a text message and EuiIcon type to -// describe how the actual value compares to the typical. -export function getMetricChangeDescription(actualProp, typicalProp) { - if (actualProp === undefined || typicalProp === undefined) { - return { iconType: 'empty', message: '' }; - } - - let iconType; - let message; - - // For metric functions, actual and typical will be single value arrays. - let actual = actualProp; - let typical = typicalProp; - if (Array.isArray(actualProp)) { - if (actualProp.length === 1) { - actual = actualProp[0]; - } else { - // TODO - do we want to enhance the description depending on detector? - // e.g. 'Unusual location' if using a lat_long detector. - return { - iconType: 'alert', - message: i18n.translate('xpack.ml.formatters.metricChangeDescription.unusualValuesDescription', { - defaultMessage: 'Unusual values', - }), - }; - } - } - - if (Array.isArray(typicalProp)) { - if (typicalProp.length === 1) { - typical = typicalProp[0]; - } - } - - if (actual === typical) { - // Very unlikely, but just in case. - message = i18n.translate('xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription', { - defaultMessage: 'actual same as typical', - }); - } else { - // For actual / typical gives output of the form: - // 4 / 2 2x higher - // 2 / 10 5x lower - // 1000 / 1 More than 100x higher - // 999 / 1000 Unusually low - // 100 / -100 Unusually high - // 0 / 100 Unexpected zero value - // 1 / 0 Unexpected non-zero value - const isHigher = actual > typical; - iconType = isHigher ? 'sortUp' : 'sortDown'; - if (typical !== 0 && actual !== 0) { - const factor = isHigher ? actual / typical : typical / actual; - if (factor > 1.5) { - if (factor <= 100) { - message = isHigher ? i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThanOneAndHalfxHigherDescription', { - defaultMessage: '{factor}x higher', - values: { factor: Math.round(factor) }, - }) : i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThanOneAndHalfxLowerDescription', { - defaultMessage: '{factor}x lower', - values: { factor: Math.round(factor) }, - }); - } else { - message = isHigher ? i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription', { - defaultMessage: 'More than 100x higher', - }) : i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription', { - defaultMessage: 'More than 100x lower', - }); - } - } else if (factor >= 1.05) { - message = isHigher ? i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThanOneAndFiveHundredthsxHigherDescription', { - defaultMessage: '{factor}x higher', - values: { factor: factor.toPrecision(2) }, - }) : i18n.translate('xpack.ml.formatters.metricChangeDescription.moreThanOneAndFiveHundredthsxLowerDescription', { - defaultMessage: '{factor}x lower', - values: { factor: factor.toPrecision(2) }, - }); - } else { - message = isHigher ? i18n.translate('xpack.ml.formatters.metricChangeDescription.unusuallyHighDescription', { - defaultMessage: 'Unusually high', - }) : i18n.translate('xpack.ml.formatters.metricChangeDescription.unusuallyLowDescription', { - defaultMessage: 'Unusually low', - }); - } - - } else { - if (actual === 0) { - message = i18n.translate('xpack.ml.formatters.metricChangeDescription.unexpectedZeroValueDescription', { - defaultMessage: 'Unexpected zero value', - }); - } else { - message = i18n.translate('xpack.ml.formatters.metricChangeDescription.unexpectedNonZeroValueDescription', { - defaultMessage: 'Unexpected non-zero value', - }); - } - } - } - - return { iconType, message }; -} - -// TODO - remove the filter once all uses of the metricChangeDescription Angular filter have been removed. -module.filter('metricChangeDescription', function () { - return function (actual, typical) { - - const { - iconType, - message - } = getMetricChangeDescription(actual, typical); - - switch (iconType) { - case 'sortUp': - return ` ${message}`; - case 'sortDown': - return ` ${message}`; - case 'alert': - return ` ${message}`; - } - - return message; - }; -}); - diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index 97f0e037e2648..c3216773c1a32 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -6,11 +6,11 @@ @import '@elastic/eui/src/components/panel/mixins'; // ML has it's own variables for coloring -@import 'variables'; +@import 'application/variables'; // Kibana management page ML section #kibanaManagementMLSection { - @import 'management/index'; + @import 'application/management/index'; } // Protect the rest of Kibana from ML generic namespacing @@ -18,33 +18,33 @@ #ml-app { // App level - @import 'app'; + @import 'application/app'; // Sub applications - @import 'data_frame_analytics/index'; - @import 'datavisualizer/index'; - @import 'explorer/index'; // SASSTODO: This file needs to be rewritten - @import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems - @import 'overview/index'; - @import 'settings/index'; - @import 'timeseriesexplorer/index'; + @import 'application/data_frame_analytics/index'; + @import 'application/datavisualizer/index'; + @import 'application/explorer/index'; // SASSTODO: This file needs to be rewritten + @import 'application/jobs/index'; // SASSTODO: This collection of sass files has multiple problems + @import 'application/overview/index'; + @import 'application/settings/index'; + @import 'application/timeseriesexplorer/index'; // Components - @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/chart_tooltip/index'; - @import 'components/controls/index'; - @import 'components/entity_cell/index'; - @import 'components/field_title_bar/index'; - @import 'components/field_type_icon/index'; - @import 'components/influencers_list/index'; - @import 'components/items_grid/index'; - @import 'components/job_selector/index'; - @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'components/navigation_menu/index'; - @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/stats_bar/index'; + @import 'application/components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/chart_tooltip/index'; + @import 'application/components/controls/index'; + @import 'application/components/entity_cell/index'; + @import 'application/components/field_title_bar/index'; + @import 'application/components/field_type_icon/index'; + @import 'application/components/influencers_list/index'; + @import 'application/components/items_grid/index'; + @import 'application/components/job_selector/index'; + @import 'application/components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner + @import 'application/components/navigation_menu/index'; + @import 'application/components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/stats_bar/index'; // Hacks are last so they can overwrite anything above if needed - @import 'hacks'; + @import 'application/hacks'; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts deleted file mode 100644 index 35e9c3326a4cc..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts +++ /dev/null @@ -1,112 +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 { Breadcrumb } from 'ui/chrome'; -import { - ANOMALY_DETECTION_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - ML_BREADCRUMB, -} from '../breadcrumbs'; - -export function getJobManagementBreadcrumbs(): Breadcrumb[] { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, - ]; -} - -export function getCreateJobBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, - ]; -} - -export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { - defaultMessage: 'Single metric', - }), - href: '', - }, - ]; -} - -export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { - defaultMessage: 'Multi metric', - }), - href: '', - }, - ]; -} - -export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { - defaultMessage: 'Population', - }), - href: '', - }, - ]; -} - -export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { - defaultMessage: 'Advanced configuration', - }), - href: '', - }, - ]; -} - -export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: $routeParams.id, - href: '', - }, - ]; -} - -export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { - defaultMessage: 'Select index or search', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.tsx deleted file mode 100644 index c23fdef324da7..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.tsx +++ /dev/null @@ -1,270 +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 React, { FC, useState, ChangeEvent } from 'react'; - -import { - EuiButtonIcon, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiToolTip, - EuiTextArea, -} from '@elastic/eui'; - -import { toastNotifications } from 'ui/notify'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; -import { getTestUrl } from './utils'; - -import { parseInterval } from '../../../../common/util/parse_interval'; -import { TIME_RANGE_TYPE } from './constants'; -import { KibanaUrlConfig } from '../../../../common/types/custom_urls'; -import { Job } from '../../new_job/common/job_creator/configs'; - -function isValidTimeRange(timeRange: KibanaUrlConfig['time_range']): boolean { - // Allow empty timeRange string, which gives the 'auto' behaviour. - if (timeRange === undefined || timeRange.length === 0 || timeRange === TIME_RANGE_TYPE.AUTO) { - return true; - } - - const interval = parseInterval(timeRange); - return interval !== null; -} - -export interface CustomUrlListProps { - job: Job; - customUrls: KibanaUrlConfig[]; - setCustomUrls: (customUrls: KibanaUrlConfig[]) => {}; -} - -/* - * React component for listing the custom URLs added to a job, - * with buttons for testing and deleting each custom URL. - */ -export const CustomUrlList: FC = ({ job, customUrls, setCustomUrls }) => { - const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); - - const onLabelChange = (e: ChangeEvent, index: number) => { - if (index < customUrls.length) { - customUrls[index] = { - ...customUrls[index], - url_name: e.target.value, - }; - setCustomUrls(customUrls); - } - }; - - const onUrlValueChange = ( - e: ChangeEvent, - index: number - ) => { - if (index < customUrls.length) { - customUrls[index] = { - ...customUrls[index], - url_value: e.target.value, - }; - setCustomUrls(customUrls); - } - }; - - const onTimeRangeChange = (e: ChangeEvent, index: number) => { - if (index < customUrls.length) { - customUrls[index] = { - ...customUrls[index], - }; - - const timeRange = e.target.value; - if (timeRange !== undefined && timeRange.length > 0) { - customUrls[index].time_range = timeRange; - } else { - delete customUrls[index].time_range; - } - setCustomUrls(customUrls); - } - }; - - const onDeleteButtonClick = (index: number) => { - if (index < customUrls.length) { - customUrls.splice(index, 1); - setCustomUrls(customUrls); - } - }; - - const onTestButtonClick = (index: number) => { - if (index < customUrls.length) { - getTestUrl(job, customUrls[index]) - .then(testUrl => { - openCustomUrlWindow(testUrl, customUrls[index]); - }) - .catch(resp => { - // eslint-disable-next-line no-console - console.error('Error obtaining URL for test:', resp); - toastNotifications.addDanger( - i18n.translate( - 'xpack.ml.customUrlEditorList.obtainingUrlToTestConfigurationErrorMessage', - { - defaultMessage: 'An error occurred obtaining the URL to test the configuration', - } - ) - ); - }); - } - }; - - const customUrlRows = customUrls.map((customUrl, index) => { - // Validate the label. - const label = customUrl.url_name; - const otherUrls = [...customUrls]; - otherUrls.splice(index, 1); // Don't compare label with itself. - const isInvalidLabel = !isValidLabel(label, otherUrls); - const invalidLabelError = isInvalidLabel - ? [ - i18n.translate('xpack.ml.customUrlEditorList.labelIsNotUniqueErrorMessage', { - defaultMessage: 'A unique label must be supplied', - }), - ] - : []; - - // Validate the time range. - const timeRange = customUrl.time_range; - const isInvalidTimeRange = !isValidTimeRange(timeRange); - const invalidIntervalError = isInvalidTimeRange - ? [ - i18n.translate('xpack.ml.customUrlEditorList.invalidTimeRangeFormatErrorMessage', { - defaultMessage: 'Invalid format', - }), - ] - : []; - - return ( - - - - } - isInvalid={isInvalidLabel} - error={invalidLabelError} - > - onLabelChange(e, index)} - /> - - - - - } - > - {index === expandedUrlIndex ? ( - { - if (input) { - input.focus(); - } - }} - fullWidth={true} - value={customUrl.url_value} - onChange={e => onUrlValueChange(e, index)} - onBlur={() => { - setExpandedUrlIndex(null); - }} - data-test-subj={`mlJobEditCustomUrlTextarea_${index}`} - /> - ) : ( - setExpandedUrlIndex(index)} - data-test-subj={`mlJobEditCustomUrlInput_${index}`} - /> - )} - - - - - } - error={invalidIntervalError} - isInvalid={isInvalidTimeRange} - > - onTimeRangeChange(e, index)} - /> - - - - - - } - > - onTestButtonClick(index)} - iconType="popout" - aria-label={i18n.translate('xpack.ml.customUrlEditorList.testCustomUrlAriaLabel', { - defaultMessage: 'Test custom URL', - })} - /> - - - - - - - } - > - onDeleteButtonClick(index)} - iconType="trash" - aria-label={i18n.translate( - 'xpack.ml.customUrlEditorList.deleteCustomUrlAriaLabel', - { - defaultMessage: 'Delete custom URL', - } - )} - /> - - - - - ); - }); - - return <>{customUrlRows}; -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.js b/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.js deleted file mode 100644 index 83317b505d599..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.js +++ /dev/null @@ -1,374 +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 { - TIME_RANGE_TYPE, - URL_TYPE -} from './constants'; - -import chrome from 'ui/chrome'; -import rison from 'rison-node'; - -import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; -import { parseInterval } from '../../../../common/util/parse_interval'; -import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; -import { ml } from '../../../services/ml_api_service'; -import { mlJobService } from '../../../services/job_service'; -import { escapeForElasticsearchQuery } from '../../../util/string_utils'; - - -export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { - // Returns the settings object in the format used by the custom URL editor - // for a new custom URL. - const kibanaSettings = { - queryFieldNames: [] - }; - - // Set the default type. - let urlType = URL_TYPE.OTHER; - if (dashboards !== undefined && dashboards.length > 0) { - urlType = URL_TYPE.KIBANA_DASHBOARD; - kibanaSettings.dashboardId = dashboards[0].id; - } else if (indexPatterns !== undefined && indexPatterns.length > 0) { - urlType = URL_TYPE.KIBANA_DISCOVER; - } - - // For the Discover option, set the default index pattern to that - // which matches the (first) index configured in the job datafeed. - const datafeedConfig = job.datafeed_config; - if (indexPatterns !== undefined && indexPatterns.length > 0 && - datafeedConfig !== undefined && - datafeedConfig.indices !== undefined && - datafeedConfig.indices.length > 0) { - - const datafeedIndex = datafeedConfig.indices[0]; - let defaultIndexPattern = indexPatterns.find((indexPattern) => { - return indexPattern.title === datafeedIndex; - }); - - if (defaultIndexPattern === undefined) { - defaultIndexPattern = indexPatterns[0]; - } - - kibanaSettings.discoverIndexPatternId = defaultIndexPattern.id; - } - - return { - label: '', - type: urlType, - // Note timeRange is only editable in new URLs for Dashboard and Discover URLs, - // as for other URLs we have no way of knowing how the field will be used in the URL. - timeRange: { - type: TIME_RANGE_TYPE.AUTO, - interval: '' - }, - kibanaSettings, - otherUrlSettings: { - urlValue: '' - } - }; -} - -export function getQueryEntityFieldNames(job) { - // Returns the list of partitioning and influencer field names that can be used - // as entities to add to the query used when linking to a Kibana dashboard or Discover. - const influencers = job.analysis_config.influencers; - const detectors = job.analysis_config.detectors; - const entityFieldNames = []; - if (influencers !== undefined) { - entityFieldNames.push(...influencers); - } - - detectors.forEach((detector, detectorIndex) => { - const partitioningFields = getPartitioningFieldNames(job, detectorIndex); - - partitioningFields.forEach((fieldName) => { - if (entityFieldNames.indexOf(fieldName) === -1) { - entityFieldNames.push(fieldName); - } - }); - }); - - return entityFieldNames; -} - -export function isValidCustomUrlSettingsTimeRange(timeRangeSettings) { - if (timeRangeSettings.type === TIME_RANGE_TYPE.INTERVAL) { - const interval = parseInterval(timeRangeSettings.interval); - return (interval !== null); - } - - return true; -} - -export function isValidCustomUrlSettings(settings, savedCustomUrls) { - let isValid = isValidLabel(settings.label, savedCustomUrls); - if (isValid === true) { - isValid = isValidCustomUrlSettingsTimeRange(settings.timeRange); - } - return isValid; -} - -export function buildCustomUrlFromSettings(settings) { - // Dashboard URL returns a Promise as a query is made to obtain the full dashboard config. - // So wrap the other two return types in a Promise for consistent return type. - if (settings.type === URL_TYPE.KIBANA_DASHBOARD) { - return buildDashboardUrlFromSettings(settings); - } else if (settings.type === URL_TYPE.KIBANA_DISCOVER) { - return Promise.resolve(buildDiscoverUrlFromSettings(settings)); - } else { - const urlToAdd = { - url_name: settings.label, - url_value: settings.otherUrlSettings.urlValue - }; - - return Promise.resolve(urlToAdd); - } - -} - -function buildDashboardUrlFromSettings(settings) { - // Get the complete list of attributes for the selected dashboard (query, filters). - return new Promise((resolve, reject) => { - const { dashboardId, queryFieldNames } = settings.kibanaSettings; - - const savedObjectsClient = chrome.getSavedObjectsClient(); - savedObjectsClient.get('dashboard', dashboardId) - .then((response) => { - // Use the filters from the saved dashboard if there are any. - let filters = []; - - // Use the query from the dashboard only if no job entities are selected. - let query = undefined; - - const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON'); - if (searchSourceJSON !== undefined) { - const searchSourceData = JSON.parse(searchSourceJSON); - if (searchSourceData.filter !== undefined) { - filters = searchSourceData.filter; - } - query = searchSourceData.query; - } - - // Add time settings to the global state URL parameter with $earliest$ and - // $latest$ tokens which get substituted for times around the time of the - // anomaly on which the URL will be run against. - const _g = rison.encode({ - time: { - from: '$earliest$', - to: '$latest$', - mode: 'absolute' - } - }); - - const appState = { - filters - }; - - // To put entities in filters section would involve creating parameters of the form - // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, - // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) - // which includes the ID of the index holding the field used in the filter. - - // So for simplicity, put entities in the query, replacing any query which is there already. - // e.g. query:(language:kuery,query:'region:us-east-1%20and%20instance:i-20d061fa') - const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); - if (queryFromEntityFieldNames !== undefined) { - query = queryFromEntityFieldNames; - } - - if (query !== undefined) { - appState.query = query; - } - - const _a = rison.encode(appState); - - const urlValue = `kibana#/dashboard/${dashboardId}?_g=${_g}&_a=${_a}`; - - const urlToAdd = { - url_name: settings.label, - url_value: urlValue, - time_range: TIME_RANGE_TYPE.AUTO - }; - - if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { - urlToAdd.time_range = settings.timeRange.interval; - } - - resolve(urlToAdd); - }) - .catch((resp) => { - reject(resp); - }); - - }); - -} - -function buildDiscoverUrlFromSettings(settings) { - const { discoverIndexPatternId, queryFieldNames } = settings.kibanaSettings; - - // Add time settings to the global state URL parameter with $earliest$ and - // $latest$ tokens which get substituted for times around the time of the - // anomaly on which the URL will be run against. - const _g = rison.encode({ - time: { - from: '$earliest$', - to: '$latest$', - mode: 'absolute' - } - }); - - // Add the index pattern and query to the appState part of the URL. - const appState = { - index: discoverIndexPatternId - }; - - // If partitioning field entities have been configured add tokens - // to the URL to use in the Discover page search. - - // Ideally we would put entities in the filters section, but currently this involves creating parameters of the form - // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, - // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) - // which includes the ID of the index holding the field used in the filter. - - // So for simplicity, put entities in the query, replacing any query which is there already. - // e.g. query:(language:kuery,query:'region:us-east-1%20and%20instance:i-20d061fa') - const queryFromEntityFieldNames = buildAppStateQueryParam(queryFieldNames); - if (queryFromEntityFieldNames !== undefined) { - appState.query = queryFromEntityFieldNames; - } - - const _a = rison.encode(appState); - - const urlValue = `kibana#/discover?_g=${_g}&_a=${_a}`; - - const urlToAdd = { - url_name: settings.label, - url_value: urlValue, - time_range: TIME_RANGE_TYPE.AUTO, - }; - - if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { - urlToAdd.time_range = settings.timeRange.interval; - } - - return urlToAdd; - -} - -// Builds the query parameter for use in the _a AppState part of a Kibana Dashboard or Discover URL. -function buildAppStateQueryParam(queryFieldNames) { - let queryParam; - if (queryFieldNames !== undefined && queryFieldNames.length > 0) { - let queryString = ''; - queryFieldNames.forEach((fieldName, i) => { - if (i > 0) { - queryString += ' and '; - } - queryString += `${escapeForElasticsearchQuery(fieldName)}:"$${fieldName}$"`; - }); - - queryParam = { - language: 'kuery', - query: queryString - }; - } - - return queryParam; -} - -// Builds the full URL for testing out a custom URL configuration, which -// may contain dollar delimited partition / influencer entity tokens and -// drilldown time range settings. -export function getTestUrl(job, customUrl) { - const urlValue = customUrl.url_value; - const bucketSpanSecs = parseInterval(job.analysis_config.bucket_span).asSeconds(); - - // By default, return configured url_value. Look to substitute any dollar-delimited - // tokens with values from the highest scoring anomaly, or if no anomalies, with - // values from a document returned by the search in the job datafeed. - let testUrl = customUrl.url_value; - - // Query to look for the highest scoring anomaly. - const body = { - query: { - bool: { - must: [ - { term: { job_id: job.job_id } }, - { term: { result_type: 'record' } } - ] - } - }, - size: 1, - _source: { - excludes: [] - }, - sort: [ - { record_score: { order: 'desc' } } - ] - }; - - return new Promise((resolve, reject) => { - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - rest_total_hits_as_int: true, - body - }) - .then((resp) => { - if (resp.hits.total > 0) { - const record = resp.hits.hits[0]._source; - testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, record, 'timestamp'); - resolve(testUrl); - } else { - // No anomalies yet for this job, so do a preview of the search - // configured in the job datafeed to obtain sample docs. - mlJobService.searchPreview(job) - .then((response) => { - let testDoc; - const docTimeFieldName = job.data_description.time_field; - - // Handle datafeeds which use aggregations or documents. - if (response.aggregations) { - // Create a dummy object which contains the fields necessary to build the URL. - const firstBucket = response.aggregations.buckets.buckets[0]; - testDoc = { - [docTimeFieldName]: firstBucket.key - }; - - // Look for bucket aggregations which match the tokens in the URL. - urlValue.replace((/\$([^?&$\'"]{1,40})\$/g), (match, name) => { - if (name !== 'earliest' && name !== 'latest' && firstBucket[name] !== undefined) { - const tokenBuckets = firstBucket[name]; - if (tokenBuckets.buckets) { - testDoc[name] = tokenBuckets.buckets[0].key; - } - } - }); - - } else { - if (response.hits.total > 0) { - testDoc = response.hits.hits[0]._source; - } - } - - if (testDoc !== undefined) { - testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, testDoc, docTimeFieldName); - } - - resolve(testUrl); - - }); - } - - }) - .catch((resp) => { - reject(resp); - }); - }); - -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js deleted file mode 100644 index 08c8993585da2..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js +++ /dev/null @@ -1,355 +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 { each } from 'lodash'; -import { toastNotifications } from 'ui/notify'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar'; -import rison from 'rison-node'; -import chrome from 'ui/chrome'; - -import { mlJobService } from 'plugins/ml/services/job_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states'; -import { parseInterval } from '../../../../common/util/parse_interval'; -import { i18n } from '@kbn/i18n'; - -export function loadFullJob(jobId) { - return new Promise((resolve, reject) => { - ml.jobs.jobs(jobId) - .then((jobs) => { - if (jobs.length) { - resolve(jobs[0]); - } else { - throw new Error(`Could not find job ${jobId}`); - } - }) - .catch((error) => { - reject(error); - }); - }); -} - -export function isStartable(jobs) { - return jobs.some(j => j.datafeedState === DATAFEED_STATE.STOPPED); -} - -export function isStoppable(jobs) { - return jobs.some(j => j.datafeedState === DATAFEED_STATE.STARTED); -} - -export function isClosable(jobs) { - return jobs.some(j => (j.datafeedState === DATAFEED_STATE.STOPPED) && (j.jobState !== JOB_STATE.CLOSED)); -} - -export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { - const datafeedIds = jobs.filter(j => j.hasDatafeed).map(j => j.datafeedId); - mlJobService.forceStartDatafeeds(datafeedIds, start, end) - .then((resp) => { - showResults(resp, DATAFEED_STATE.STARTED); - finish(); - }) - .catch((error) => { - mlMessageBarService.notify.error(error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { - defaultMessage: 'Jobs failed to start' - }), error); - finish(); - }); -} - - -export function stopDatafeeds(jobs, finish = () => {}) { - const datafeedIds = jobs.filter(j => j.hasDatafeed).map(j => j.datafeedId); - mlJobService.stopDatafeeds(datafeedIds) - .then((resp) => { - showResults(resp, DATAFEED_STATE.STOPPED); - finish(); - }) - .catch((error) => { - mlMessageBarService.notify.error(error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { - defaultMessage: 'Jobs failed to stop' - }), error); - finish(); - }); -} - -function showResults(resp, action) { - const successes = []; - const failures = []; - for (const d in resp) { - if (resp[d][action] === true || - (resp[d][action] === false && (resp[d].error.statusCode === 409 && action === DATAFEED_STATE.STARTED))) { - successes.push(d); - } else { - failures.push({ - id: d, - result: resp[d] - }); - } - } - - let actionText = ''; - let actionTextPT = ''; - if (action === DATAFEED_STATE.STARTED) { - actionText = i18n.translate('xpack.ml.jobsList.startActionStatusText', { - defaultMessage: 'start' - }); - actionTextPT = i18n.translate('xpack.ml.jobsList.startedActionStatusText', { - defaultMessage: 'started' - }); - } else if (action === DATAFEED_STATE.STOPPED) { - actionText = i18n.translate('xpack.ml.jobsList.stopActionStatusText', { - defaultMessage: 'stop' - }); - actionTextPT = i18n.translate('xpack.ml.jobsList.stoppedActionStatusText', { - defaultMessage: 'stopped' - }); - } else if (action === DATAFEED_STATE.DELETED) { - actionText = i18n.translate('xpack.ml.jobsList.deleteActionStatusText', { - defaultMessage: 'delete' - }); - actionTextPT = i18n.translate('xpack.ml.jobsList.deletedActionStatusText', { - defaultMessage: 'deleted' - }); - } else if (action === JOB_STATE.CLOSED) { - actionText = i18n.translate('xpack.ml.jobsList.closeActionStatusText', { - defaultMessage: 'close' - }); - actionTextPT = i18n.translate('xpack.ml.jobsList.closedActionStatusText', { - defaultMessage: 'closed' - }); - } - - toastNotifications.addSuccess(i18n.translate('xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage', { - defaultMessage: '{successesJobsCount, plural, one{{successJob}} other{# jobs}} {actionTextPT} successfully', - values: { - successesJobsCount: successes.length, - successJob: successes[0], - actionTextPT - } - })); - - if (failures.length > 0) { - failures.forEach((f) => { - mlMessageBarService.notify.error(f.result.error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.actionFailedNotificationMessage', { - defaultMessage: '{failureId} failed to {actionText}', - values: { - failureId: f.id, - actionText - } - })); - }); - } -} - -export function cloneJob(jobId) { - loadFullJob(jobId) - .then((job) => { - if(job.custom_settings && job.custom_settings.created_by) { - // if the job is from a wizards, i.e. contains a created_by property - // use tempJobCloningObjects to temporarily store the job - mlJobService.tempJobCloningObjects.job = job; - - if ( - job.data_counts.earliest_record_timestamp !== undefined && - job.data_counts.latest_record_timestamp !== undefined && - job.data_counts.latest_bucket_timestamp !== undefined) { - // if the job has run before, use the earliest and latest record timestamp - // as the cloned job's time range - let start = job.data_counts.earliest_record_timestamp; - let end = job.data_counts.latest_record_timestamp; - - if (job.datafeed_config.aggregations !== undefined) { - // if the datafeed uses aggregations the earliest and latest record timestamps may not be the same - // as the start and end of the data in the index. - const bucketSpanMs = parseInterval(job.analysis_config.bucket_span).asMilliseconds(); - // round down to the start of the nearest bucket - start = Math.floor(job.data_counts.earliest_record_timestamp / bucketSpanMs) * bucketSpanMs; - // use latest_bucket_timestamp and add two bucket spans minus one ms - end = job.data_counts.latest_bucket_timestamp + (bucketSpanMs * 2) - 1; - } - - mlJobService.tempJobCloningObjects.start = start; - mlJobService.tempJobCloningObjects.end = end; - } - } else { - // otherwise use the tempJobCloningObjects - mlJobService.tempJobCloningObjects.job = job; - } - window.location.href = '#/jobs/new_job'; - }) - .catch((error) => { - mlMessageBarService.notify.error(error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { - defaultMessage: 'Could not clone {jobId}. Job could not be found', - values: { jobId } - })); - }); -} - -export function closeJobs(jobs, finish = () => {}) { - const jobIds = jobs.map(j => j.id); - mlJobService.closeJobs(jobIds) - .then((resp) => { - showResults(resp, JOB_STATE.CLOSED); - finish(); - }) - .catch((error) => { - mlMessageBarService.notify.error(error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { - defaultMessage: 'Jobs failed to close', - }), error); - finish(); - }); -} - -export function deleteJobs(jobs, finish = () => {}) { - const jobIds = jobs.map(j => j.id); - mlJobService.deleteJobs(jobIds) - .then((resp) => { - showResults(resp, JOB_STATE.DELETED); - finish(); - }) - .catch((error) => { - mlMessageBarService.notify.error(error); - toastNotifications.addDanger(i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { - defaultMessage: 'Jobs failed to delete', - }), error); - finish(); - }); -} - -export function filterJobs(jobs, clauses) { - if (clauses.length === 0) { - return jobs; - } - - // keep count of the number of matches we make as we're looping over the clauses - // we only want to return jobs which match all clauses, i.e. each search term is ANDed - const matches = jobs.reduce((p, c) => { - p[c.id] = { - job: c, - count: 0 - }; - return p; - }, {}); - - clauses.forEach((c) => { - // the search term could be negated with a minus, e.g. -bananas - const bool = (c.match === 'must'); - let js = []; - - if (c.type === 'term') { - // filter term based clauses, e.g. bananas - // match on id, description and memory_status - // if the term has been negated, AND the matches - if (bool === true) { - js = jobs.filter(job => (( - (stringMatch(job.id, c.value) === bool) || - (stringMatch(job.description, c.value) === bool) || - (stringMatch(job.memory_status, c.value) === bool) - ))); - } else { - js = jobs.filter(job => (( - (stringMatch(job.id, c.value) === bool) && - (stringMatch(job.description, c.value) === bool) && - (stringMatch(job.memory_status, c.value) === bool) - ))); - } - } else { - // filter other clauses, i.e. the toggle group buttons - if (Array.isArray(c.value)) { - // the groups value is an array of group ids - js = jobs.filter(job => (jobProperty(job, c.field).some(g => (c.value.indexOf(g) >= 0)))); - } else { - js = jobs.filter(job => (jobProperty(job, c.field) === c.value)); - } - } - - js.forEach(j => (matches[j.id].count++)); - }); - - // loop through the matches and return only those jobs which have match all the clauses - const filteredJobs = []; - each(matches, (m) => { - if (m.count >= clauses.length) { - filteredJobs.push(m.job); - } - }); - return filteredJobs; -} - -// check to see if a job has been stored in mlJobService.tempJobCloningObjects -// if it has, return an object with the minimum properties needed for the -// start datafeed modal. -export function checkForAutoStartDatafeed() { - const job = mlJobService.tempJobCloningObjects.job; - if (job !== undefined) { - mlJobService.tempJobCloningObjects.job = undefined; - const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0); - const datafeedId = hasDatafeed ? job.datafeed_config.datafeed_id : ''; - return { - id: job.job_id, - hasDatafeed, - latestTimestampSortValue: 0, - datafeedId, - }; - } -} - -function stringMatch(str, substr) { - return ( - (typeof str === 'string' && typeof substr === 'string') && - ((str.toLowerCase().match(substr.toLowerCase()) === null) === false) - ); -} - -function jobProperty(job, prop) { - const propMap = { - job_state: 'jobState', - datafeed_state: 'datafeedState', - groups: 'groups', - }; - return job[propMap[prop]]; -} - -export function getJobIdUrl(jobId) { - // Create url for filtering by job id for kibana management table - const settings = { - jobId - }; - const encoded = rison.encode(settings); - const url = `?mlManagement=${encoded}`; - - return `${chrome.getBasePath()}/app/ml#/jobs${url}`; -} - -function getUrlVars(url) { - const vars = {}; - url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) { - vars[key] = value; - }); - return vars; -} - -export function getSelectedJobIdFromUrl(url) { - if (typeof (url) === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - return decodedJson.jobId; - } -} - -export function clearSelectedJobIdFromUrl(url) { - if (typeof (url) === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const clearedParams = `ml#/jobs?_g=${urlParams._g}`; - window.history.replaceState({}, document.title, clearedParams); - } -} - diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js deleted file mode 100644 index 4b6f3f485d49d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js +++ /dev/null @@ -1,59 +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 ReactDOM from 'react-dom'; -import React from 'react'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { loadIndexPatterns } from 'plugins/ml/util/index_utils'; -import { checkFullLicense } from 'plugins/ml/license/check_license'; -import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { getJobManagementBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { loadMlServerInfo } from 'plugins/ml/services/ml_server_info'; - -import uiRoutes from 'ui/routes'; - -const template = ``; - -uiRoutes - .when('/jobs/?', { - template, - k7Breadcrumbs: getJobManagementBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - indexPatterns: loadIndexPatterns, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - } - }); - -import { JobsPage } from './jobs'; -import { I18nContext } from 'ui/i18n'; - -module.directive('jobsPage', function () { - return { - scope: {}, - restrict: 'E', - link: (scope, element) => { - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - } - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx deleted file mode 100644 index cb3dbb59f894a..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx +++ /dev/null @@ -1,100 +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 React, { Fragment, FC, useContext, useState } from 'react'; - -import { JobCreatorContext } from '../../../job_creator_context'; -import { AdvancedJobCreator } from '../../../../../common/job_creator'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Aggregation, Field } from '../../../../../../../../common/types/fields'; -import { MetricSelector } from './metric_selector'; -import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; -import { DetectorList } from './detector_list'; -import { ModalPayload } from '../advanced_detector_modal/advanced_detector_modal'; - -interface Props { - setIsValid: (na: boolean) => void; -} - -const emptyRichDetector: RichDetector = { - agg: null, - field: null, - byField: null, - overField: null, - partitionField: null, - excludeFrequent: null, - description: null, - customRules: null, -}; - -export const AdvancedDetectors: FC = ({ setIsValid }) => { - const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext); - const jobCreator = jc as AdvancedJobCreator; - - const { fields, aggs } = newJobCapsService; - const [modalPayload, setModalPayload] = useState(null); - - function closeModal() { - setModalPayload(null); - } - - function detectorChangeHandler(dtr: RichDetector, index?: number) { - if (index === undefined) { - jobCreator.addDetector( - dtr.agg as Aggregation, - dtr.field as Field, - dtr.byField, - dtr.overField, - dtr.partitionField, - dtr.excludeFrequent, - dtr.description - ); - } else { - jobCreator.editDetector( - dtr.agg as Aggregation, - dtr.field as Field, - dtr.byField, - dtr.overField, - dtr.partitionField, - dtr.excludeFrequent, - dtr.description, - index - ); - } - jobCreatorUpdate(); - setModalPayload(null); - } - - function showModal() { - setModalPayload({ detector: emptyRichDetector }); - } - - function onDeleteJob(i: number) { - jobCreator.removeDetector(i); - jobCreatorUpdate(); - } - - function onEditJob(i: number) { - const dtr = jobCreator.richDetectors[i]; - if (dtr !== undefined) { - setModalPayload({ detector: dtr, index: i }); - } - } - - return ( - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx deleted file mode 100644 index 5bc38ca934165..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx +++ /dev/null @@ -1,59 +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 React, { FC, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { Aggregation, Field } from '../../../../../../../../common/types/fields'; -import { AdvancedDetectorModal, ModalPayload } from '../advanced_detector_modal'; -import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; - -interface Props { - payload: ModalPayload | null; - fields: Field[]; - aggs: Aggregation[]; - detectorChangeHandler: (dtr: RichDetector) => void; - closeModal(): void; - showModal(): void; -} - -const MAX_WIDTH = 560; - -export const MetricSelector: FC = ({ - payload, - fields, - aggs, - detectorChangeHandler, - closeModal, - showModal, -}) => { - return ( - - - - - - - - - - - - {payload !== null && ( - - )} - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx deleted file mode 100644 index e4dd46b159a6c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ /dev/null @@ -1,80 +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 React, { Fragment, FC } from 'react'; -import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; - -import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; -import { ChartSettings } from '../../../charts/common/settings'; -import { LineChartData } from '../../../../../common/chart_loader'; -import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; -import { SplitCards, useAnimateSplit } from '../split_cards'; -import { DetectorTitle } from '../detector_title'; -import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; - -interface ChartGridProps { - aggFieldPairList: AggFieldPair[]; - chartSettings: ChartSettings; - splitField: SplitField; - fieldValues: string[]; - lineChartsData: LineChartData; - modelData: Record; - anomalyData: Record; - deleteDetector?: (index: number) => void; - jobType: JOB_TYPE; - animate?: boolean; - loading?: boolean; -} - -export const ChartGrid: FC = ({ - aggFieldPairList, - chartSettings, - splitField, - fieldValues, - lineChartsData, - modelData, - anomalyData, - deleteDetector, - jobType, - loading = false, -}) => { - const animateSplit = useAnimateSplit(); - - return ( - - - {aggFieldPairList.map((af, i) => ( - - - - - - - ))} - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx deleted file mode 100644 index b0a5049924cbc..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ /dev/null @@ -1,181 +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 React, { Fragment, FC, useContext, useEffect, useState } from 'react'; - -import { JobCreatorContext } from '../../../job_creator_context'; -import { MultiMetricJobCreator } from '../../../../../common/job_creator'; -import { LineChartData } from '../../../../../common/chart_loader'; -import { DropDownLabel, DropDownProps } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair } from '../../../../../../../../common/types/fields'; -import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; -import { MetricSelector } from './metric_selector'; -import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; - -interface Props { - setIsValid: (na: boolean) => void; -} - -export const MultiMetricDetectors: FC = ({ setIsValid }) => { - const { - jobCreator: jc, - jobCreatorUpdate, - jobCreatorUpdated, - chartLoader, - chartInterval, - } = useContext(JobCreatorContext); - - const jobCreator = jc as MultiMetricJobCreator; - - const { fields } = newJobCapsService; - const [selectedOptions, setSelectedOptions] = useState([]); - const [aggFieldPairList, setAggFieldPairList] = useState( - jobCreator.aggFieldPairs - ); - const [lineChartsData, setLineChartsData] = useState({}); - const [loadingData, setLoadingData] = useState(false); - const [start, setStart] = useState(jobCreator.start); - const [end, setEnd] = useState(jobCreator.end); - const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); - const [chartSettings, setChartSettings] = useState(defaultChartSettings); - const [splitField, setSplitField] = useState(jobCreator.splitField); - const [fieldValues, setFieldValues] = useState([]); - const [pageReady, setPageReady] = useState(false); - - function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { - addDetector(selectedOptionsIn); - } - - function addDetector(selectedOptionsIn: DropDownLabel[]) { - if (selectedOptionsIn !== null && selectedOptionsIn.length) { - const option = selectedOptionsIn[0] as DropDownLabel; - if (typeof option !== 'undefined') { - const newPair = { agg: option.agg, field: option.field }; - setAggFieldPairList([...aggFieldPairList, newPair]); - setSelectedOptions([]); - } else { - setAggFieldPairList([]); - } - } - } - - function deleteDetector(index: number) { - aggFieldPairList.splice(index, 1); - setAggFieldPairList([...aggFieldPairList]); - } - - useEffect(() => { - setPageReady(true); - }, []); - - // watch for changes in detector list length - useEffect(() => { - jobCreator.removeAllDetectors(); - aggFieldPairList.forEach(pair => { - jobCreator.addDetector(pair.agg, pair.field); - }); - jobCreator.calculateModelMemoryLimit(); - jobCreatorUpdate(); - loadCharts(); - setIsValid(aggFieldPairList.length > 0); - }, [aggFieldPairList.length]); - - // watch for change in jobCreator - useEffect(() => { - if (jobCreator.start !== start || jobCreator.end !== end) { - setStart(jobCreator.start); - setEnd(jobCreator.end); - loadCharts(); - } - - if (jobCreator.bucketSpanMs !== bucketSpanMs) { - setBucketSpanMs(jobCreator.bucketSpanMs); - loadCharts(); - } - - setSplitField(jobCreator.splitField); - }, [jobCreatorUpdated]); - - // watch for changes in split field. - // load example field values - // changes to fieldValues here will trigger the card effect - useEffect(() => { - if (splitField !== null) { - chartLoader - .loadFieldExampleValues(splitField) - .then(setFieldValues) - .catch(error => { - mlMessageBarService.notify.error(error); - }); - } else { - setFieldValues([]); - } - jobCreator.calculateModelMemoryLimit(); - }, [splitField]); - - // watch for changes in the split field values - // reload the charts - useEffect(() => { - loadCharts(); - }, [fieldValues]); - - async function loadCharts() { - if (allDataReady()) { - setLoadingData(true); - try { - const cs = getChartSettings(jobCreator, chartInterval); - setChartSettings(cs); - const resp: LineChartData = await chartLoader.loadLineCharts( - jobCreator.start, - jobCreator.end, - aggFieldPairList, - jobCreator.splitField, - fieldValues.length > 0 ? fieldValues[0] : null, - cs.intervalMs - ); - setLineChartsData(resp); - } catch (error) { - mlMessageBarService.notify.error(error); - setLineChartsData([]); - } - setLoadingData(false); - } - } - - function allDataReady() { - return ( - pageReady && - aggFieldPairList.length > 0 && - (splitField === null || (splitField !== null && fieldValues.length > 0)) - ); - } - - return ( - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx deleted file mode 100644 index 42bdc2a19deda..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; -import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; - -interface Props { - fields: Field[]; - detectorChangeHandler: (options: DropDownLabel[]) => void; - selectedOptions: DropDownProps; - maxWidth?: number; - removeOptions: AggFieldPair[]; -} - -const MAX_WIDTH = 560; - -export const MetricSelector: FC = ({ - fields, - detectorChangeHandler, - selectedOptions, - maxWidth, - removeOptions, -}) => { - return ( - - - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx deleted file mode 100644 index e3374be22485c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx +++ /dev/null @@ -1,90 +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 React, { Fragment, FC } from 'react'; -import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; - -import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; -import { ChartSettings } from '../../../charts/common/settings'; -import { LineChartData } from '../../../../../common/chart_loader'; -import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; -import { SplitCards, useAnimateSplit } from '../split_cards'; -import { DetectorTitle } from '../detector_title'; -import { ByFieldSelector } from '../split_field'; -import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; - -type DetectorFieldValues = Record; - -interface ChartGridProps { - aggFieldPairList: AggFieldPair[]; - chartSettings: ChartSettings; - splitField: SplitField; - lineChartsData: LineChartData; - modelData: Record; - anomalyData: Record; - deleteDetector?: (index: number) => void; - jobType: JOB_TYPE; - fieldValuesPerDetector: DetectorFieldValues; - loading?: boolean; -} - -export const ChartGrid: FC = ({ - aggFieldPairList, - chartSettings, - splitField, - lineChartsData, - modelData, - anomalyData, - deleteDetector, - jobType, - fieldValuesPerDetector, - loading = false, -}) => { - const animateSplit = useAnimateSplit(); - - return ( - - {aggFieldPairList.map((af, i) => ( - - - - - - {deleteDetector !== undefined && } - - {jobType === JOB_TYPE.POPULATION && } - - - - - - - - ))} - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx deleted file mode 100644 index 9a24381c9e35f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ /dev/null @@ -1,241 +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 React, { Fragment, FC, useContext, useEffect, useState, useReducer } from 'react'; -import { EuiHorizontalRule } from '@elastic/eui'; - -import { JobCreatorContext } from '../../../job_creator_context'; -import { PopulationJobCreator } from '../../../../../common/job_creator'; -import { LineChartData } from '../../../../../common/chart_loader'; -import { DropDownLabel, DropDownProps } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; -import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; -import { MetricSelector } from './metric_selector'; -import { SplitFieldSelector } from '../split_field'; -import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; - -interface Props { - setIsValid: (na: boolean) => void; -} - -type DetectorFieldValues = Record; - -export const PopulationDetectors: FC = ({ setIsValid }) => { - const { - jobCreator: jc, - jobCreatorUpdate, - jobCreatorUpdated, - chartLoader, - chartInterval, - } = useContext(JobCreatorContext); - const jobCreator = jc as PopulationJobCreator; - - const { fields } = newJobCapsService; - const [selectedOptions, setSelectedOptions] = useState([]); - const [aggFieldPairList, setAggFieldPairList] = useState( - jobCreator.aggFieldPairs - ); - const [lineChartsData, setLineChartsData] = useState({}); - const [loadingData, setLoadingData] = useState(false); - const [start, setStart] = useState(jobCreator.start); - const [end, setEnd] = useState(jobCreator.end); - const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); - const [chartSettings, setChartSettings] = useState(defaultChartSettings); - const [splitField, setSplitField] = useState(jobCreator.splitField); - const [fieldValuesPerDetector, setFieldValuesPerDetector] = useState({}); - const [byFieldsUpdated, setByFieldsUpdated] = useReducer<(s: number) => number>(s => s + 1, 0); - const [pageReady, setPageReady] = useState(false); - const updateByFields = () => setByFieldsUpdated(0); - - function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { - addDetector(selectedOptionsIn); - } - - function addDetector(selectedOptionsIn: DropDownLabel[]) { - if (selectedOptionsIn !== null && selectedOptionsIn.length) { - const option = selectedOptionsIn[0] as DropDownLabel; - if (typeof option !== 'undefined') { - const newPair = { agg: option.agg, field: option.field, by: { field: null, value: null } }; - setAggFieldPairList([...aggFieldPairList, newPair]); - setSelectedOptions([]); - } else { - setAggFieldPairList([]); - } - } - } - - function deleteDetector(index: number) { - aggFieldPairList.splice(index, 1); - setAggFieldPairList([...aggFieldPairList]); - updateByFields(); - } - - useEffect(() => { - setPageReady(true); - }, []); - - // watch for changes in detector list length - useEffect(() => { - jobCreator.removeAllDetectors(); - aggFieldPairList.forEach((pair, i) => { - jobCreator.addDetector(pair.agg, pair.field); - if (pair.by !== undefined) { - // re-add by fields - jobCreator.setByField(pair.by.field, i); - } - }); - jobCreatorUpdate(); - loadCharts(); - setIsValid(aggFieldPairList.length > 0); - }, [aggFieldPairList.length]); - - // watch for changes in by field values - // redraw the charts if they change. - // triggered when example fields have been loaded - // if the split field or by fields have changed - useEffect(() => { - loadCharts(); - }, [JSON.stringify(fieldValuesPerDetector), splitField, pageReady]); - - // watch for change in jobCreator - useEffect(() => { - if (jobCreator.start !== start || jobCreator.end !== end) { - setStart(jobCreator.start); - setEnd(jobCreator.end); - loadCharts(); - } - - if (jobCreator.bucketSpanMs !== bucketSpanMs) { - setBucketSpanMs(jobCreator.bucketSpanMs); - loadCharts(); - } - - setSplitField(jobCreator.splitField); - - // update by fields and their by fields - let update = false; - const newList = [...aggFieldPairList]; - newList.forEach((pair, i) => { - const bf = jobCreator.getByField(i); - if (pair.by !== undefined && pair.by.field !== bf) { - pair.by.field = bf; - update = true; - } - }); - if (update) { - setAggFieldPairList(newList); - updateByFields(); - } - }, [jobCreatorUpdated]); - - // watch for changes in split field or by fields. - // load example field values - // changes to fieldValues here will trigger the card effect via setFieldValuesPerDetector - useEffect(() => { - loadFieldExamples(); - }, [splitField, byFieldsUpdated]); - - async function loadCharts() { - if (allDataReady()) { - setLoadingData(true); - try { - const cs = getChartSettings(jobCreator, chartInterval); - setChartSettings(cs); - const resp: LineChartData = await chartLoader.loadPopulationCharts( - jobCreator.start, - jobCreator.end, - aggFieldPairList, - jobCreator.splitField, - cs.intervalMs - ); - - setLineChartsData(resp); - } catch (error) { - mlMessageBarService.notify.error(error); - setLineChartsData([]); - } - setLoadingData(false); - } - } - - async function loadFieldExamples() { - const promises: any[] = []; - aggFieldPairList.forEach((af, i) => { - if (af.by !== undefined && af.by.field !== null) { - promises.push( - (async (index: number, field: Field) => { - return { - index, - fields: await chartLoader.loadFieldExampleValues(field), - }; - })(i, af.by.field) - ); - } - }); - const results = await Promise.all(promises); - const fieldValues = results.reduce((p, c) => { - p[c.index] = c.fields; - return p; - }, {}) as DetectorFieldValues; - - const newPairs = aggFieldPairList.map((pair, i) => ({ - ...pair, - ...(pair.by === undefined || pair.by.field === null - ? {} - : { - by: { - ...pair.by, - value: fieldValues[i][0], - }, - }), - })); - setAggFieldPairList([...newPairs]); - setFieldValuesPerDetector(fieldValues); - } - - function allDataReady() { - let ready = aggFieldPairList.length > 0; - aggFieldPairList.forEach(af => { - if (af.by !== undefined && af.by.field !== null) { - // if a by field is set, it's only ready when the value is loaded - ready = ready && af.by.value !== null; - } - }); - return ready; - } - - return ( - - - {splitField !== null && } - - {splitField !== null && ( - - )} - {splitField !== null && ( - - )} - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx deleted file mode 100644 index b831f9033f977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; -import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; - -interface Props { - fields: Field[]; - detectorChangeHandler: (options: DropDownLabel[]) => void; - selectedOptions: DropDownProps; - maxWidth?: number; - removeOptions: AggFieldPair[]; -} - -const MAX_WIDTH = 560; - -export const MetricSelector: FC = ({ - fields, - detectorChangeHandler, - selectedOptions, - maxWidth, - removeOptions, -}) => { - return ( - - - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx deleted file mode 100644 index 19ccca44dc0a5..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ /dev/null @@ -1,126 +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 React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { JobCreatorContext } from '../../../job_creator_context'; -import { SingleMetricJobCreator } from '../../../../../common/job_creator'; -import { LineChartData } from '../../../../../common/chart_loader'; -import { AggSelect, DropDownLabel, DropDownProps, createLabel } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair } from '../../../../../../../../common/types/fields'; -import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; -import { getChartSettings } from '../../../charts/common/settings'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; - -interface Props { - setIsValid: (na: boolean) => void; -} - -const DTR_IDX = 0; - -export const SingleMetricDetectors: FC = ({ setIsValid }) => { - const { - jobCreator: jc, - jobCreatorUpdate, - jobCreatorUpdated, - chartLoader, - chartInterval, - } = useContext(JobCreatorContext); - const jobCreator = jc as SingleMetricJobCreator; - - const { fields } = newJobCapsService; - const [selectedOptions, setSelectedOptions] = useState( - jobCreator.aggFieldPair !== null ? [{ label: createLabel(jobCreator.aggFieldPair) }] : [] - ); - const [aggFieldPair, setAggFieldPair] = useState(jobCreator.aggFieldPair); - const [lineChartsData, setLineChartData] = useState({}); - const [loadingData, setLoadingData] = useState(false); - const [start, setStart] = useState(jobCreator.start); - const [end, setEnd] = useState(jobCreator.end); - const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); - - function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { - setSelectedOptions(selectedOptionsIn); - if (selectedOptionsIn.length) { - const option = selectedOptionsIn[0]; - if (typeof option !== 'undefined') { - setAggFieldPair({ agg: option.agg, field: option.field }); - } else { - setAggFieldPair(null); - } - } - } - - useEffect(() => { - if (aggFieldPair !== null) { - jobCreator.setDetector(aggFieldPair.agg, aggFieldPair.field); - jobCreatorUpdate(); - loadChart(); - setIsValid(aggFieldPair !== null); - } - }, [aggFieldPair]); - - useEffect(() => { - if (jobCreator.start !== start || jobCreator.end !== end) { - setStart(jobCreator.start); - setEnd(jobCreator.end); - loadChart(); - } - - if (jobCreator.bucketSpanMs !== bucketSpanMs) { - setBucketSpanMs(jobCreator.bucketSpanMs); - loadChart(); - } - }, [jobCreatorUpdated]); - - async function loadChart() { - if (aggFieldPair !== null) { - setLoadingData(true); - try { - const cs = getChartSettings(jobCreator, chartInterval); - const resp: LineChartData = await chartLoader.loadLineCharts( - jobCreator.start, - jobCreator.end, - [aggFieldPair], - null, - null, - cs.intervalMs - ); - if (resp[DTR_IDX] !== undefined) { - setLineChartData(resp); - } - } catch (error) { - mlMessageBarService.notify.error(error); - setLineChartData({}); - } - setLoadingData(false); - } - } - - return ( - - - {(lineChartsData[DTR_IDX] !== undefined || loadingData === true) && ( - - - - )} - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/__test__/directive.js deleted file mode 100644 index bd63a16abfacd..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; - -describe('ML - Index or Saved Search selection directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Index or Saved Search selection directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/directive.tsx deleted file mode 100644 index 7f3edf0896840..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/directive.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { Page } from './page'; - -module.directive('mlIndexOrSearch', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const $route = $injector.get('$route'); - const { nextStepPath } = $route.current.locals; - - ReactDOM.render( - {React.createElement(Page, { nextStepPath })}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/__test__/directive.js deleted file mode 100644 index 5be526f2eb2c0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; - -describe('ML - Job Type Directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Job Type Directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/directive.tsx deleted file mode 100644 index 59dff64c1cd78..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/directive.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; -import { IndexPatterns } from 'ui/index_patterns'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../new_job/utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; - -module.directive('mlJobTypePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/directive.tsx deleted file mode 100644 index 1725211861c0c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/directive.tsx +++ /dev/null @@ -1,76 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; -import { IndexPatterns } from 'ui/index_patterns'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../utils/new_job_utils'; -import { Page, PageProps } from './page'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; - -module.directive('mlNewJobPage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; - - if ($route.current.locals.jobType === undefined) { - return; - } - const jobType: JOB_TYPE = $route.current.locals.jobType; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - const props: PageProps = { - existingJobsAndGroups, - jobType, - }; - - ReactDOM.render( - - - {React.createElement(Page, props)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/__test__/directive.js deleted file mode 100644 index 7cbf22bf45ec5..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; - -describe('ML - Recognize job directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Recognize job directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/directive.tsx deleted file mode 100644 index 1882296f96418..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/directive.tsx +++ /dev/null @@ -1,68 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; -import { IndexPatterns } from 'ui/index_patterns'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../common/types/angular'; - -import { createSearchItems } from '../../new_job/utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; - -module.directive('mlRecognizePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const moduleId = $route.current.params.id; - const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page, { moduleId, existingGroupIds })} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/page.tsx deleted file mode 100644 index f9a5230ef17d9..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/page.tsx +++ /dev/null @@ -1,335 +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 React, { FC, useState, Fragment, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiPage, - EuiPageBody, - EuiTitle, - EuiPageHeaderSection, - EuiPageHeader, - EuiFlexItem, - EuiFlexGroup, - EuiText, - EuiSpacer, - EuiCallOut, - EuiPanel, -} from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { merge } from 'lodash'; -import { ml } from '../../../services/ml_api_service'; -import { useKibanaContext } from '../../../contexts/kibana'; -import { - DatafeedResponse, - DataRecognizerConfigResponse, - JobOverride, - JobResponse, - KibanaObject, - KibanaObjectResponse, - Module, - ModuleJob, -} from '../../../../common/types/modules'; -import { mlJobService } from '../../../services/job_service'; -import { CreateResultCallout } from './components/create_result_callout'; -import { KibanaObjects } from './components/kibana_objects'; -import { ModuleJobs } from './components/module_jobs'; -import { checkForSavedObjects } from './resolvers'; -import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form'; -import { TimeRange } from '../common/components'; -import { JobId } from '../common/job_creator/configs'; - -export interface ModuleJobUI extends ModuleJob { - datafeedResult?: DatafeedResponse; - setupResult?: JobResponse; -} - -export type KibanaObjectUi = KibanaObject & KibanaObjectResponse; - -export interface KibanaObjects { - [objectType: string]: KibanaObjectUi[]; -} - -interface PageProps { - moduleId: string; - existingGroupIds: string[]; -} - -export type JobOverrides = Record; - -export enum SAVE_STATE { - NOT_SAVED, - SAVING, - SAVED, - FAILED, - PARTIAL_FAILURE, -} - -export const Page: FC = ({ moduleId, existingGroupIds }) => { - // #region State - const [jobPrefix, setJobPrefix] = useState(''); - const [jobs, setJobs] = useState([]); - const [jobOverrides, setJobOverrides] = useState({}); - const [kibanaObjects, setKibanaObjects] = useState({}); - const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); - const [resultsUrl, setResultsUrl] = useState(''); - // #endregion - - const { - currentSavedSearch: savedSearch, - currentIndexPattern: indexPattern, - combinedQuery, - } = useKibanaContext(); - const pageTitle = - savedSearch.id !== undefined - ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title }, - }) - : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { - defaultMessage: 'index pattern {indexPatternTitle}', - values: { indexPatternTitle: indexPattern.title }, - }); - const displayQueryWarning = savedSearch.id !== undefined; - const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; - - /** - * Loads recognizer module configuration. - */ - const loadModule = async () => { - try { - const response: Module = await ml.getDataRecognizerModule({ moduleId }); - setJobs(response.jobs); - - const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects); - setKibanaObjects(kibanaObjectsResult); - - setSaveState(SAVE_STATE.NOT_SAVED); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } - }; - - const getTimeRange = async ( - useFullIndexData: boolean, - timeRange: TimeRange - ): Promise => { - if (useFullIndexData) { - const { start, end } = await ml.getTimeFieldRange({ - index: indexPattern.title, - timeFieldName: indexPattern.timeFieldName, - query: combinedQuery, - }); - return { - start: start.epoch, - end: end.epoch, - }; - } else { - return Promise.resolve(timeRange); - } - }; - - useEffect(() => { - loadModule(); - }, []); - - /** - * Sets up recognizer module configuration. - */ - const save = async (formValues: JobSettingsFormValues) => { - setSaveState(SAVE_STATE.SAVING); - const { - jobPrefix: resultJobPrefix, - startDatafeedAfterSave, - useDedicatedIndex, - useFullIndexData, - timeRange, - } = formValues; - - const resultTimeRange = await getTimeRange(useFullIndexData, timeRange); - - try { - let jobOverridesPayload: JobOverride[] | null = Object.values(jobOverrides); - jobOverridesPayload = jobOverridesPayload.length > 0 ? jobOverridesPayload : null; - - const response: DataRecognizerConfigResponse = await ml.setupDataRecognizerConfig({ - moduleId, - prefix: resultJobPrefix, - query: tempQuery, - indexPatternName: indexPattern.title, - useDedicatedIndex, - startDatafeed: startDatafeedAfterSave, - ...(jobOverridesPayload !== null ? { jobOverrides: jobOverridesPayload } : {}), - ...resultTimeRange, - }); - const { datafeeds: datafeedsResponse, jobs: jobsResponse, kibana: kibanaResponse } = response; - - setJobs( - jobs.map(job => { - return { - ...job, - datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)), - setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id), - }; - }) - ); - setKibanaObjects(merge(kibanaObjects, kibanaResponse)); - setResultsUrl( - mlJobService.createResultsUrl( - jobsResponse.filter(({ success }) => success).map(({ id }) => id), - resultTimeRange.start, - resultTimeRange.end, - 'explorer' - ) - ); - const failedJobsCount = jobsResponse.reduce((count, { success }) => { - return success ? count : count + 1; - }, 0); - setSaveState( - failedJobsCount === 0 - ? SAVE_STATE.SAVED - : failedJobsCount === jobs.length - ? SAVE_STATE.FAILED - : SAVE_STATE.PARTIAL_FAILURE - ); - } catch (e) { - setSaveState(SAVE_STATE.FAILED); - // eslint-disable-next-line no-console - console.error('Error setting up module', e); - toastNotifications.addDanger({ - title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { - defaultMessage: 'Error setting up module {moduleId}', - values: { moduleId }, - }), - text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', { - defaultMessage: - 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', - values: { - count: jobs.length, - }, - }), - }); - } - }; - - const onJobOverridesChange = (job: JobOverride) => { - setJobOverrides({ - ...jobOverrides, - [job.job_id as string]: job, - }); - }; - - const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); - - return ( - - - - - -

- -

-
-
-
- - {displayQueryWarning && ( - <> - - } - color="warning" - iconType="alert" - > - - - - - - - )} - - - - - -

- -

-
- - - - {isFormVisible && ( - { - setJobPrefix(formValues.jobPrefix); - }} - saveState={saveState} - jobs={jobs} - /> - )} - -
-
- - - - - {Object.keys(kibanaObjects).length > 0 && ( - <> - - - {Object.keys(kibanaObjects).map((objectType, i) => ( - - - {i < Object.keys(kibanaObjects).length - 1 && } - - ))} - - - )} - -
- -
-
- ); -}; diff --git a/x-pack/legacy/plugins/ml/public/management/_index.scss b/x-pack/legacy/plugins/ml/public/management/_index.scss deleted file mode 100644 index d527197a5c2c6..0000000000000 --- a/x-pack/legacy/plugins/ml/public/management/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './jobs_list/index'; diff --git a/x-pack/legacy/plugins/ml/public/management/index.ts b/x-pack/legacy/plugins/ml/public/management/index.ts deleted file mode 100644 index 744b03c3d592b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/management/index.ts +++ /dev/null @@ -1,41 +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. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { management } from 'ui/management'; -// @ts-ignore No declaration file for module -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { i18n } from '@kbn/i18n'; -import { JOBS_LIST_PATH } from './management_urls'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import 'plugins/ml/management/jobs_list'; - -if ( - xpackInfo.get('features.ml.showLinks', false) === true && - xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL -) { - management.register('ml', { - display: i18n.translate('xpack.ml.management.mlTitle', { - defaultMessage: 'Machine Learning', - }), - order: 100, - icon: 'machineLearningApp', - }); - - management.getSection('ml').register('jobsList', { - name: 'jobsListLink', - order: 10, - display: i18n.translate('xpack.ml.management.jobsListTitle', { - defaultMessage: 'Jobs list', - }), - url: `#${JOBS_LIST_PATH}`, - }); -} diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/_index.scss deleted file mode 100644 index 192091fb04e3c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss deleted file mode 100644 index 883ecd96745b4..0000000000000 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './jobs_list_page/stats_bar'; -@import './jobs_list_page/buttons'; -@import './jobs_list_page/expanded_row'; -@import './jobs_list_page/analytics_table'; diff --git a/x-pack/legacy/plugins/ml/public/overview/_index.scss b/x-pack/legacy/plugins/ml/public/overview/_index.scss deleted file mode 100644 index 192091fb04e3c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts deleted file mode 100644 index 893ae5de450ad..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts +++ /dev/null @@ -1,23 +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'; -// @ts-ignore -import { ML_BREADCRUMB } from '../breadcrumbs'; - -export function getOverviewBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overviewBreadcrumbs.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx deleted file mode 100644 index 7ee9cff107db8..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx +++ /dev/null @@ -1,132 +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 React, { FC, useState } from 'react'; -import { EuiBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - MlInMemoryTable, - SortDirection, - SORT_DIRECTION, - OnTableChangeArg, - ColumnType, -} from '../../../components/ml_in_memory_table'; -import { getAnalysisType } from '../../../data_frame_analytics/common/analytics'; -import { - DataFrameAnalyticsListColumn, - DataFrameAnalyticsListRow, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { - getTaskStateBadge, - progressColumn, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns'; -import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions'; -import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; - -interface Props { - items: any[]; -} -export const AnalyticsTable: FC = ({ items }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - - const [sortField, setSortField] = useState(DataFrameAnalyticsListColumn.id); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - - // id, type, status, progress, created time, view icon - const columns: ColumnType[] = [ - { - field: DataFrameAnalyticsListColumn.id, - name: i18n.translate('xpack.ml.overview.analyticsList.id', { defaultMessage: 'ID' }), - sortable: true, - truncateText: true, - width: '20%', - }, - { - name: i18n.translate('xpack.ml.overview.analyticsList.type', { defaultMessage: 'Type' }), - sortable: (item: DataFrameAnalyticsListRow) => getAnalysisType(item.config.analysis), - truncateText: true, - render(item: DataFrameAnalyticsListRow) { - return {getAnalysisType(item.config.analysis)}; - }, - width: '150px', - }, - { - name: i18n.translate('xpack.ml.overview.analyticsList.status', { defaultMessage: 'Status' }), - sortable: (item: DataFrameAnalyticsListRow) => item.stats.state, - truncateText: true, - render(item: DataFrameAnalyticsListRow) { - return getTaskStateBadge(item.stats.state, item.stats.reason); - }, - width: '100px', - }, - progressColumn, - { - field: DataFrameAnalyticsListColumn.configCreateTime, - name: i18n.translate('xpack.ml.overview.analyticsList.reatedTimeColumnName', { - defaultMessage: 'Creation time', - }), - dataType: 'date', - render: (time: number) => formatHumanReadableDateTimeSeconds(time), - textOnly: true, - truncateText: true, - sortable: true, - width: '20%', - }, - { - name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', { - defaultMessage: 'Actions', - }), - actions: [AnalyticsViewAction], - width: '100px', - }, - ]; - - const onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: DataFrameAnalyticsListColumn.id, direction: SORT_DIRECTION.ASC }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: items.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - return ( - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/actions.tsx deleted file mode 100644 index e865fd44c2a19..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/actions.tsx +++ /dev/null @@ -1,44 +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 React, { FC } from 'react'; -import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -// @ts-ignore no module file -import { getLink } from '../../../jobs/jobs_list/components/job_actions/results'; -import { MlSummaryJobs } from '../../../../common/types/jobs'; - -interface Props { - jobsList: MlSummaryJobs; -} - -export const ExplorerLink: FC = ({ jobsList }) => { - const openJobsInAnomalyExplorerText = i18n.translate( - 'xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText', - { - defaultMessage: 'Open {jobsCount, plural, one {{jobId}} other {# jobs}} in Anomaly Explorer', - values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id }, - } - ); - - return ( - - - {i18n.translate('xpack.ml.overview.anomalyDetection.exploreActionName', { - defaultMessage: 'Explore', - })} - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/table.tsx b/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/table.tsx deleted file mode 100644 index 9c863f115685b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/table.tsx +++ /dev/null @@ -1,231 +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 React, { FC, Fragment, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiIcon, - EuiLoadingSpinner, - EuiSpacer, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - MlInMemoryTable, - SortDirection, - SORT_DIRECTION, - OnTableChangeArg, - ColumnType, -} from '../../../components/ml_in_memory_table'; -import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; -import { ExplorerLink } from './actions'; -import { getJobsFromGroup } from './utils'; -import { GroupsDictionary, Group } from './anomaly_detection_panel'; -import { MlSummaryJobs } from '../../../../common/types/jobs'; -import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar'; -// @ts-ignore -import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge'; -import { toLocaleString } from '../../../util/string_utils'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; - -// Used to pass on attribute names to table columns -export enum AnomalyDetectionListColumns { - id = 'id', - maxAnomalyScore = 'max_anomaly_score', - jobIds = 'jobIds', - latestTimestamp = 'latest_timestamp', - docsProcessed = 'docs_processed', - jobsInGroup = 'jobs_in_group', -} - -interface Props { - items: GroupsDictionary; - statsBarData: JobStatsBarStats; - jobsList: MlSummaryJobs; -} - -export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData }) => { - const groupsList = Object.values(items); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - - const [sortField, setSortField] = useState(AnomalyDetectionListColumns.id); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - - // columns: group, max anomaly, jobs in group, latest timestamp, docs processed, action to explorer - const columns: ColumnType[] = [ - { - field: AnomalyDetectionListColumns.id, - name: i18n.translate('xpack.ml.overview.anomalyDetection.tableId', { - defaultMessage: 'Group ID', - }), - render: (id: Group['id']) => , - sortable: true, - truncateText: true, - width: '20%', - }, - { - field: AnomalyDetectionListColumns.maxAnomalyScore, - name: ( - - - {i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', { - defaultMessage: 'Max anomaly score', - })}{' '} - - - - ), - sortable: true, - render: (score: Group['max_anomaly_score']) => { - if (score === undefined) { - // score is not loaded yet - return ; - } else if (score === null) { - // an error occurred loading this group's score - return ( - - - - ); - } else if (score === 0) { - return ( - // @ts-ignore - - {score} - - ); - } else { - const color: string = getSeverityColor(score); - return ( - // @ts-ignore - - {score >= 1 ? Math.floor(score) : '< 1'} - - ); - } - }, - truncateText: true, - width: '150px', - }, - { - field: AnomalyDetectionListColumns.jobsInGroup, - name: i18n.translate('xpack.ml.overview.anomalyDetection.tableNumJobs', { - defaultMessage: 'Jobs in group', - }), - sortable: true, - truncateText: true, - width: '100px', - }, - { - field: AnomalyDetectionListColumns.latestTimestamp, - name: i18n.translate('xpack.ml.overview.anomalyDetection.tableLatestTimestamp', { - defaultMessage: 'Latest timestamp', - }), - dataType: 'date', - render: (time: number) => formatHumanReadableDateTimeSeconds(time), - textOnly: true, - truncateText: true, - sortable: true, - width: '20%', - }, - { - field: AnomalyDetectionListColumns.docsProcessed, - name: i18n.translate('xpack.ml.overview.anomalyDetection.tableDocsProcessed', { - defaultMessage: 'Docs processed', - }), - render: (docs: number) => toLocaleString(docs), - textOnly: true, - sortable: true, - width: '20%', - }, - { - name: i18n.translate('xpack.ml.overview.anomalyDetection.tableActionLabel', { - defaultMessage: 'Actions', - }), - render: (group: Group) => , - width: '100px', - align: 'right', - }, - ]; - - const onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: AnomalyDetectionListColumns.id, direction: SORT_DIRECTION.ASC }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: groupsList.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - return ( - - - - -

- {i18n.translate('xpack.ml.overview.anomalyDetection.panelTitle', { - defaultMessage: 'Anomaly Detection', - })} -

-
-
- - - -
- - -
- ); -}; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/utils.ts b/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/utils.ts deleted file mode 100644 index db369d9228e6c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/utils.ts +++ /dev/null @@ -1,179 +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 { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; -import { Group, GroupsDictionary } from './anomaly_detection_panel'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs'; - -export function getGroupsFromJobs( - jobs: MlSummaryJobs -): { groups: GroupsDictionary; count: number } { - const groups: any = { - ungrouped: { - id: 'ungrouped', - jobIds: [], - docs_processed: 0, - latest_timestamp: 0, - max_anomaly_score: null, - jobs_in_group: 0, - }, - }; - - jobs.forEach((job: MlSummaryJob) => { - // Organize job by group - if (job.groups.length > 0) { - job.groups.forEach((g: any) => { - if (groups[g] === undefined) { - groups[g] = { - id: g, - jobIds: [job.id], - docs_processed: job.processed_record_count, - latest_timestamp: job.latestTimestampMs, - max_anomaly_score: null, - jobs_in_group: 1, - }; - } else { - groups[g].jobIds.push(job.id); - groups[g].docs_processed += job.processed_record_count; - groups[g].jobs_in_group++; - // if incoming job latest timestamp is greater than the last saved one, replace it - if (groups[g].latest_timestamp === undefined) { - groups[g].latest_timestamp = job.latestTimestampMs; - } else if (job.latestTimestampMs > groups[g].latest_timestamp) { - groups[g].latest_timestamp = job.latestTimestampMs; - } - } - }); - } else { - groups.ungrouped.jobIds.push(job.id); - groups.ungrouped.docs_processed += job.processed_record_count; - groups.ungrouped.jobs_in_group++; - // if incoming job latest timestamp is greater than the last saved one, replace it - if (job.latestTimestampMs > groups.ungrouped.latest_timestamp) { - groups.ungrouped.latest_timestamp = job.latestTimestampMs; - } - } - }); - - if (groups.ungrouped.jobIds.length === 0) { - delete groups.ungrouped; - } - - const count = Object.values(groups).length; - - return { groups, count }; -} - -export function getStatsBarData(jobsList: any) { - const jobStats = { - activeNodes: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', { - defaultMessage: 'Active ML Nodes', - }), - value: 0, - show: true, - }, - total: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', { - defaultMessage: 'Total jobs', - }), - value: 0, - show: true, - }, - open: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.openJobsLabel', { - defaultMessage: 'Open jobs', - }), - value: 0, - show: true, - }, - closed: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.closedJobsLabel', { - defaultMessage: 'Closed jobs', - }), - value: 0, - show: true, - }, - failed: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.failedJobsLabel', { - defaultMessage: 'Failed jobs', - }), - value: 0, - show: false, - }, - activeDatafeeds: { - label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', { - defaultMessage: 'Active datafeeds', - }), - value: 0, - show: true, - }, - }; - - if (jobsList === undefined) { - return jobStats; - } - - // object to keep track of nodes being used by jobs - const mlNodes: any = {}; - let failedJobs = 0; - - jobsList.forEach((job: MlSummaryJob) => { - if (job.jobState === JOB_STATE.OPENED) { - jobStats.open.value++; - } else if (job.jobState === JOB_STATE.CLOSED) { - jobStats.closed.value++; - } else if (job.jobState === JOB_STATE.FAILED) { - failedJobs++; - } - - if (job.hasDatafeed && job.datafeedState === DATAFEED_STATE.STARTED) { - jobStats.activeDatafeeds.value++; - } - - if (job.nodeName !== undefined) { - mlNodes[job.nodeName] = {}; - } - }); - - jobStats.total.value = jobsList.length; - - // Only show failed jobs if it is non-zero - if (failedJobs) { - jobStats.failed.value = failedJobs; - jobStats.failed.show = true; - } else { - jobStats.failed.show = false; - } - - jobStats.activeNodes.value = Object.keys(mlNodes).length; - - return jobStats; -} - -export function getJobsFromGroup(group: Group, jobs: any) { - return group.jobIds.map(jobId => jobs[jobId]).filter(id => id !== undefined); -} - -export function getJobsWithTimerange(jobsList: any) { - const jobs: any = {}; - jobsList.forEach((job: any) => { - if (jobs[job.id] === undefined) { - // create the job in the object with the times you need - if (job.earliestTimestampMs !== undefined) { - const { earliestTimestampMs, latestResultsTimestampMs } = job; - jobs[job.id] = { - id: job.id, - earliestTimestampMs, - latestResultsTimestampMs, - }; - } - } - }); - - return jobs; -} diff --git a/x-pack/legacy/plugins/ml/public/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/services/forecast_service.js deleted file mode 100644 index 4ec8a9dd1fec4..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/forecast_service.js +++ /dev/null @@ -1,382 +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. - */ - - - -// Service for carrying out requests to run ML forecasts and to obtain -// data on forecasts that have been performed. -import _ from 'lodash'; - -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; -import { ml } from './ml_api_service'; - -// Gets a basic summary of the most recently run forecasts for the specified -// job, with results at or later than the supplied timestamp. -// Extra query object can be supplied, or pass null if no additional query. -// Returned response contains a forecasts property, which is an array of objects -// containing id, earliest and latest keys. -function getForecastsSummary( - job, - query, - earliestMs, - maxResults -) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - forecasts: [] - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, result type and earliest time, plus - // the additional query if supplied. - const filterCriteria = [ - { - term: { result_type: 'model_forecast_request_stats' } - }, - { - term: { job_id: job.job_id } - }, - { - range: { - timestamp: { - gte: earliestMs, - format: 'epoch_millis' - } - } - } - ]; - - if (query) { - filterCriteria.push(query); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: maxResults, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: filterCriteria - } - }, - sort: [ - { forecast_create_timestamp: { 'order': 'desc' } } - ] - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - obj.forecasts = resp.hits.hits.map(hit => hit._source); - } - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Obtains the earliest and latest timestamps for the forecast data from -// the forecast with the specified ID. -// Returned response contains earliest and latest properties which are the -// timestamps of the first and last model_forecast results. -function getForecastDateRange(job, forecastId) { - - return new Promise((resolve, reject) => { - const obj = { - success: true, - earliest: null, - latest: null - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, forecast ID, result type and time range. - const filterCriteria = [{ - query_string: { - query: 'result_type:model_forecast', - analyze_wildcard: true - } - }, - { - term: { job_id: job.job_id } - }, - { - term: { forecast_id: forecastId } - }]; - - // TODO - add in criteria for detector index and entity fields (by, over, partition) - // once forecasting with these parameters is supported. - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: filterCriteria - } - }, - aggs: { - earliest: { - min: { - field: 'timestamp' - } - }, - latest: { - max: { - field: 'timestamp' - } - } - } - } - }) - .then((resp) => { - obj.earliest = _.get(resp, 'aggregations.earliest.value', null); - obj.latest = _.get(resp, 'aggregations.latest.value', null); - if (obj.earliest === null || obj.latest === null) { - reject(resp); - } else { - resolve(obj); - } - }) - .catch((resp) => { - reject(resp); - }); - - }); -} - -// Obtains the requested forecast model data for the forecast with the specified ID. -function getForecastData( - job, - detectorIndex, - forecastId, - entityFields, - earliestMs, - latestMs, - interval, - aggType) { - // Extract the partition, by, over fields on which to filter. - const criteriaFields = []; - const detector = job.analysis_config.detectors[detectorIndex]; - if (_.has(detector, 'partition_field_name')) { - const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); - if (partitionEntity !== undefined) { - criteriaFields.push( - { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, - { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); - } - } - - if (_.has(detector, 'over_field_name')) { - const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); - if (overEntity !== undefined) { - criteriaFields.push( - { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, - { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); - } - } - - if (_.has(detector, 'by_field_name')) { - const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); - if (byEntity !== undefined) { - criteriaFields.push( - { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, - { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); - } - } - - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, forecast ID, detector index, result type and time range. - const filterCriteria = [{ - query_string: { - query: 'result_type:model_forecast', - analyze_wildcard: true - } - }, - { - term: { job_id: job.job_id } - }, - { - term: { forecast_id: forecastId } - }, - { - term: { detector_index: detectorIndex } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }]; - - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - filterCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - - - // If an aggType object has been passed in, use it. - // Otherwise default to avg, min and max aggs for the - // forecast prediction, upper and lower - const forecastAggs = (aggType === undefined) ? - { avg: 'avg', max: 'max', min: 'min' } : - { - avg: aggType.avg, - max: aggType.max, - min: aggType.min - }; - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: filterCriteria - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 - }, - aggs: { - prediction: { - [forecastAggs.avg]: { - field: 'forecast_prediction' - } - }, - forecastUpper: { - [forecastAggs.max]: { - field: 'forecast_upper' - } - }, - forecastLower: { - [forecastAggs.min]: { - field: 'forecast_lower' - } - } - } - } - } - } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - obj.results[time] = { - prediction: _.get(dataForTime, ['prediction', 'value']), - forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), - forecastLower: _.get(dataForTime, ['forecastLower', 'value']) - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - - }); -} - -// Runs a forecast -function runForecast(jobId, duration) { - console.log('ML forecast service run forecast with duration:', duration); - return new Promise((resolve, reject) => { - - ml.forecast({ - jobId, - duration - }) - .then((resp) => { - resolve(resp); - }).catch((err) => { - reject(err); - }); - }); -} - -// Gets stats for a forecast that has been run on the specified job. -// Returned response contains a stats property, including -// forecast_progress (a value from 0 to 1), -// and forecast_status ('finished' when complete) properties. -function getForecastRequestStats(job, forecastId) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - stats: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, result type and earliest time. - const filterCriteria = [{ - query_string: { - query: 'result_type:model_forecast_request_stats', - analyze_wildcard: true - } - }, - { - term: { job_id: job.job_id } - }, - { - term: { forecast_id: forecastId } - }]; - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 1, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: filterCriteria - } - } - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - obj.stats = _.first(resp.hits.hits)._source; - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - - }); -} - -export const mlForecastService = { - getForecastsSummary, - getForecastDateRange, - getForecastData, - runForecast, - getForecastRequestStats -}; diff --git a/x-pack/legacy/plugins/ml/public/services/http_service.js b/x-pack/legacy/plugins/ml/public/services/http_service.js deleted file mode 100644 index f0bef4396e4f3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/http_service.js +++ /dev/null @@ -1,50 +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. - */ - - - -// service for interacting with the server - -import chrome from 'ui/chrome'; - -import { addSystemApiHeader } from 'ui/system_api'; - -export function http(options) { - return new Promise((resolve, reject) => { - if(options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = addSystemApiHeader({ - 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), - ...options.headers - }); - - const allHeaders = (options.headers === undefined) ? headers : { ...options.headers, ...headers }; - const body = (options.data === undefined) ? null : JSON.stringify(options.data); - - const payload = { - method: (options.method || 'GET'), - headers: allHeaders, - credentials: 'same-origin' - }; - - if (body !== null) { - payload.body = body; - } - - fetch(url, payload) - .then((resp) => { - resp.json().then((resp.ok === true) ? resolve : reject); - }) - .catch((resp) => { - reject(resp); - }); - } else { - reject(); - } - }); -} diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/annotations.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/annotations.js deleted file mode 100644 index c889d0e98ad3e..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/annotations.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import chrome from 'ui/chrome'; - -import { http } from '../../services/http_service'; - -const basePath = chrome.addBasePath('/api/ml'); - -export const annotations = { - getAnnotations(obj) { - return http({ - url: `${basePath}/annotations`, - method: 'POST', - data: obj - }); - }, - indexAnnotation(obj) { - return http({ - url: `${basePath}/annotations/index`, - method: 'PUT', - data: obj - }); - }, - deleteAnnotation(id) { - return http({ - url: `${basePath}/annotations/delete/${id}`, - method: 'DELETE' - }); - } -}; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/filters.js deleted file mode 100644 index 7f07e227e4167..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/filters.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// Service for querying filters, which hold lists of entities, -// for example a list of known safe URL domains. - -import chrome from 'ui/chrome'; - -import { http } from '../../services/http_service'; - -const basePath = chrome.addBasePath('/api/ml'); - -export const filters = { - filters(obj) { - const filterId = (obj && obj.filterId) ? `/${obj.filterId}` : ''; - return http({ - url: `${basePath}/filters${filterId}`, - method: 'GET' - }); - }, - - filtersStats() { - return http({ - url: `${basePath}/filters/_stats`, - method: 'GET' - }); - }, - - addFilter( - filterId, - description, - items) { - return http({ - url: `${basePath}/filters`, - method: 'PUT', - data: { - filterId, - description, - items - } - }); - }, - - updateFilter( - filterId, - description, - addItems, - removeItems - ) { - const data = {}; - if (description !== undefined) { - data.description = description; - } - if (addItems !== undefined) { - data.addItems = addItems; - } - if (removeItems !== undefined) { - data.removeItems = removeItems; - } - - return http({ - url: `${basePath}/filters/${filterId}`, - method: 'PUT', - data - }); - }, - - deleteFilter(filterId) { - return http({ - url: `${basePath}/filters/${filterId}`, - method: 'DELETE' - }); - }, - - -}; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts deleted file mode 100644 index 12f39bfa78dc0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ /dev/null @@ -1,180 +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 { Annotation } from '../../../common/types/annotations'; -import { AggFieldNamePair } from '../../../common/types/fields'; -import { ExistingJobsAndGroups } from '../job_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; -import { MlSummaryJobs } from '../../../common/types/jobs'; -import { MlServerDefaults, MlServerLimits } from '../../services/ml_server_info'; -import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; -import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { JobMessage } from '../../../common/types/audit_message'; -import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; -import { DeepPartial } from '../../../common/types/common'; - -// TODO This is not a complete representation of all methods of `ml.*`. -// It just satisfies needs for other parts of the code area which use -// TypeScript and rely on the methods typed in here. -// This allows the import of `ml` into TypeScript code. -interface EsIndex { - name: string; -} - -export interface GetTimeFieldRangeResponse { - success: boolean; - start: { epoch: number; string: string }; - end: { epoch: number; string: string }; -} - -export interface BucketSpanEstimatorData { - aggTypes: Array; - duration: { - start: number; - end: number; - }; - fields: Array; - index: string; - query: any; - splitField: string | undefined; - timeField: string | undefined; -} - -export interface BucketSpanEstimatorResponse { - name: string; - ms: number; - error?: boolean; - message?: { msg: string } | string; -} - -export interface MlInfoResponse { - defaults: MlServerDefaults; - limits: MlServerLimits; - native_code: { - build_hash: string; - version: string; - }; - upgrade_mode: boolean; - cloudId?: string; -} - -declare interface Ml { - annotations: { - deleteAnnotation(id: string | undefined): Promise; - indexAnnotation(annotation: Annotation): Promise; - }; - - dataFrameAnalytics: { - getDataFrameAnalytics(analyticsId?: string): Promise; - getDataFrameAnalyticsStats(analyticsId?: string): Promise; - createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise; - evaluateDataFrameAnalytics(evaluateConfig: any): Promise; - estimateDataFrameAnalyticsMemoryUsage( - jobConfig: DeepPartial - ): Promise; - deleteDataFrameAnalytics(analyticsId: string): Promise; - startDataFrameAnalytics(analyticsId: string): Promise; - stopDataFrameAnalytics( - analyticsId: string, - force?: boolean, - waitForCompletion?: boolean - ): Promise; - getAnalyticsAuditMessages(analyticsId: string): Promise; - }; - - hasPrivileges(obj: object): Promise; - - checkMlPrivileges(): Promise; - checkManageMLPrivileges(): Promise; - getJobStats(obj: object): Promise; - getDatafeedStats(obj: object): Promise; - esSearch(obj: object): any; - getIndices(): Promise; - dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; - getDataRecognizerModule(obj: { moduleId: string }): Promise; - setupDataRecognizerConfig(obj: object): Promise; - getTimeFieldRange(obj: object): Promise; - calculateModelMemoryLimit(obj: object): Promise<{ modelMemoryLimit: string }>; - calendars(): Promise< - Array<{ - calendar_id: string; - description: string; - events: any[]; - job_ids: string[]; - }> - >; - - getVisualizerFieldStats(obj: object): Promise; - getVisualizerOverallStats(obj: object): Promise; - - results: { - getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise; - }; - - jobs: { - jobsSummary(jobIds: string[]): Promise; - jobs(jobIds: string[]): Promise; - groups(): Promise; - updateGroups(updatedJobs: string[]): Promise; - forceStartDatafeeds(datafeedIds: string[], start: string, end: string): Promise; - stopDatafeeds(datafeedIds: string[]): Promise; - deleteJobs(jobIds: string[]): Promise; - closeJobs(jobIds: string[]): Promise; - jobAuditMessages(jobId: string, from?: string): Promise; - deletingJobTasks(): Promise; - newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; - newJobLineChart( - indexPatternTitle: string, - timeField: string, - start: number, - end: number, - intervalMs: number, - query: object, - aggFieldNamePairs: AggFieldNamePair[], - splitFieldName: string | null, - splitFieldValue: string | null - ): Promise; - newJobPopulationsChart( - indexPatternTitle: string, - timeField: string, - start: number, - end: number, - intervalMs: number, - query: object, - aggFieldNamePairs: AggFieldNamePair[], - splitFieldName: string - ): Promise; - getAllJobAndGroupIds(): Promise; - getLookBackProgress( - jobId: string, - start: number, - end: number - ): Promise<{ progress: number; isRunning: boolean; isJobClosed: boolean }>; - }; - - estimateBucketSpan(data: BucketSpanEstimatorData): Promise; - - mlNodeCount(): Promise<{ count: number }>; - mlInfo(): Promise; -} - -declare const ml: Ml; - -export interface GetDataFrameAnalyticsStatsResponseOk { - node_failures?: object; - count: number; - data_frame_analytics: DataFrameAnalyticsStats[]; -} - -export interface GetDataFrameAnalyticsStatsResponseError { - statusCode: number; - error: string; - message: string; -} - -export type GetDataFrameAnalyticsStatsResponse = - | GetDataFrameAnalyticsStatsResponseOk - | GetDataFrameAnalyticsStatsResponseError; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js deleted file mode 100644 index 94c79fe470236..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js +++ /dev/null @@ -1,461 +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 { pick } from 'lodash'; -import chrome from 'ui/chrome'; - -import { http } from '../../services/http_service'; - -import { annotations } from './annotations'; -import { dataFrameAnalytics } from './data_frame_analytics'; -import { filters } from './filters'; -import { results } from './results'; -import { jobs } from './jobs'; -import { fileDatavisualizer } from './datavisualizer'; - -const basePath = chrome.addBasePath('/api/ml'); - -export const ml = { - getJobs(obj) { - const jobId = (obj && obj.jobId) ? `/${obj.jobId}` : ''; - return http({ - url: `${basePath}/anomaly_detectors${jobId}`, - }); - }, - - getJobStats(obj) { - const jobId = (obj && obj.jobId) ? `/${obj.jobId}` : ''; - return http({ - url: `${basePath}/anomaly_detectors${jobId}/_stats`, - }); - }, - - addJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, - method: 'PUT', - data: obj.job - }); - }, - - openJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_open`, - method: 'POST' - }); - }, - - closeJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_close`, - method: 'POST' - }); - }, - - deleteJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, - method: 'DELETE' - }); - }, - - forceDeleteJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}?force=true`, - method: 'DELETE' - }); - }, - - updateJob(obj) { - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_update`, - method: 'POST', - data: obj.job - }); - }, - - estimateBucketSpan(obj) { - return http({ - url: `${basePath}/validate/estimate_bucket_span`, - method: 'POST', - data: obj - }); - }, - - validateJob(obj) { - return http({ - url: `${basePath}/validate/job`, - method: 'POST', - data: obj - }); - }, - - validateCardinality(obj) { - return http({ - url: `${basePath}/validate/cardinality`, - method: 'POST', - data: obj - }); - }, - - getDatafeeds(obj) { - const datafeedId = (obj && obj.datafeedId) ? `/${obj.datafeedId}` : ''; - return http({ - url: `${basePath}/datafeeds${datafeedId}`, - }); - }, - - getDatafeedStats(obj) { - const datafeedId = (obj && obj.datafeedId) ? `/${obj.datafeedId}` : ''; - return http({ - url: `${basePath}/datafeeds${datafeedId}/_stats`, - }); - }, - - addDatafeed(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, - method: 'PUT', - data: obj.datafeedConfig - }); - }, - - updateDatafeed(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_update`, - method: 'POST', - data: obj.datafeedConfig - }); - }, - - deleteDatafeed(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, - method: 'DELETE' - }); - }, - - forceDeleteDatafeed(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}?force=true`, - method: 'DELETE' - }); - }, - - startDatafeed(obj) { - const data = {}; - if(obj.start !== undefined) { - data.start = obj.start; - } - if(obj.end !== undefined) { - data.end = obj.end; - } - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_start`, - method: 'POST', - data - }); - }, - - stopDatafeed(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_stop`, - method: 'POST' - }); - }, - - datafeedPreview(obj) { - return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_preview`, - method: 'GET' - }); - }, - - validateDetector(obj) { - return http({ - url: `${basePath}/anomaly_detectors/_validate/detector`, - method: 'POST', - data: obj.detector - }); - }, - - forecast(obj) { - const data = {}; - if(obj.duration !== undefined) { - data.duration = obj.duration; - } - - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_forecast`, - method: 'POST', - data - }); - }, - - overallBuckets(obj) { - const data = pick(obj, [ - 'topN', - 'bucketSpan', - 'start', - 'end' - ]); - return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, - method: 'POST', - data - }); - }, - - hasPrivileges(obj) { - return http({ - url: `${basePath}/_has_privileges`, - method: 'POST', - data: obj - }); - }, - - checkMlPrivileges() { - return http({ - url: `${basePath}/ml_capabilities`, - method: 'GET', - }); - }, - - checkManageMLPrivileges() { - return http({ - url: `${basePath}/ml_capabilities?ignoreSpaces=true`, - method: 'GET' - }); - }, - - getNotificationSettings() { - return http({ - url: `${basePath}/notification_settings`, - method: 'GET' - }); - }, - - getFieldCaps(obj) { - const data = {}; - if(obj.index !== undefined) { - data.index = obj.index; - } - if(obj.fields !== undefined) { - data.fields = obj.fields; - } - return http({ - url: `${basePath}/indices/field_caps`, - method: 'POST', - data - }); - }, - - recognizeIndex(obj) { - return http({ - url: `${basePath}/modules/recognize/${obj.indexPatternTitle}`, - method: 'GET' - }); - }, - - listDataRecognizerModules() { - return http({ - url: `${basePath}/modules/get_module`, - method: 'GET' - }); - }, - - getDataRecognizerModule(obj) { - return http({ - url: `${basePath}/modules/get_module/${obj.moduleId}`, - method: 'GET' - }); - }, - - dataRecognizerModuleJobsExist(obj) { - return http({ - url: `${basePath}/modules/jobs_exist/${obj.moduleId}`, - method: 'GET' - }); - }, - - setupDataRecognizerConfig(obj) { - const data = pick(obj, [ - 'prefix', - 'groups', - 'indexPatternName', - 'query', - 'useDedicatedIndex', - 'startDatafeed', - 'start', - 'end', - 'jobOverrides', - ]); - - return http({ - url: `${basePath}/modules/setup/${obj.moduleId}`, - method: 'POST', - data - }); - }, - - getVisualizerFieldStats(obj) { - const data = pick(obj, [ - 'query', - 'timeFieldName', - 'earliest', - 'latest', - 'samplerShardSize', - 'interval', - 'fields', - 'maxExamples' - ]); - - return http({ - url: `${basePath}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, - method: 'POST', - data - }); - }, - - getVisualizerOverallStats(obj) { - const data = pick(obj, [ - 'query', - 'timeFieldName', - 'earliest', - 'latest', - 'samplerShardSize', - 'aggregatableFields', - 'nonAggregatableFields' - ]); - - return http({ - url: `${basePath}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, - method: 'POST', - data - }); - }, - - calendars(obj) { - const calendarId = (obj && obj.calendarId) ? `/${obj.calendarId}` : ''; - return http({ - url: `${basePath}/calendars${calendarId}`, - method: 'GET' - }); - }, - - addCalendar(obj) { - return http({ - url: `${basePath}/calendars`, - method: 'PUT', - data: obj - }); - }, - - updateCalendar(obj) { - const calendarId = (obj && obj.calendarId) ? `/${obj.calendarId}` : ''; - return http({ - url: `${basePath}/calendars${calendarId}`, - method: 'PUT', - data: obj - }); - }, - - deleteCalendar(obj) { - return http({ - url: `${basePath}/calendars/${obj.calendarId}`, - method: 'DELETE' - }); - }, - - mlNodeCount() { - return http({ - url: `${basePath}/ml_node_count`, - method: 'GET' - }); - }, - - mlInfo() { - return http({ - url: `${basePath}/info`, - method: 'GET' - }); - }, - - calculateModelMemoryLimit(obj) { - const data = pick(obj, [ - 'indexPattern', - 'splitFieldName', - 'query', - 'fieldNames', - 'influencerNames', - 'timeFieldName', - 'earliestMs', - 'latestMs' - ]); - - return http({ - url: `${basePath}/validate/calculate_model_memory_limit`, - method: 'POST', - data - }); - }, - - getCardinalityOfFields(obj) { - const data = pick(obj, [ - 'index', - 'fieldNames', - 'query', - 'timeFieldName', - 'earliestMs', - 'latestMs' - ]); - - return http({ - url: `${basePath}/fields_service/field_cardinality`, - method: 'POST', - data - }); - }, - - getTimeFieldRange(obj) { - const data = pick(obj, [ - 'index', - 'timeFieldName', - 'query' - ]); - - return http({ - url: `${basePath}/fields_service/time_field_range`, - method: 'POST', - data - }); - }, - - esSearch(obj) { - return http({ - url: `${basePath}/es_search`, - method: 'POST', - data: obj - }); - }, - - getIndices() { - const tempBasePath = chrome.addBasePath('/api'); - return http({ - url: `${tempBasePath}/index_management/indices`, - method: 'GET', - }); - }, - - annotations, - dataFrameAnalytics, - filters, - results, - jobs, - fileDatavisualizer, -}; diff --git a/x-pack/legacy/plugins/ml/public/services/results_service.d.ts b/x-pack/legacy/plugins/ml/public/services/results_service.d.ts deleted file mode 100644 index 2bbe37c3fc05d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/results_service.d.ts +++ /dev/null @@ -1,61 +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. - */ - -type time = string; -export interface ModelPlotOutputResults { - results: Record; -} - -declare interface MlResultsService { - getScoresByBucket: ( - jobIds: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - maxResults: number - ) => Promise; - getScheduledEventsByBucket: () => Promise; - getTopInfluencers: () => Promise; - getTopInfluencerValues: () => Promise; - getOverallBucketScores: ( - jobIds: any, - topN: any, - earliestMs: any, - latestMs: any, - interval?: any - ) => Promise; - getInfluencerValueMaxScoreByTime: () => Promise; - getRecordInfluencers: () => Promise; - getRecordsForInfluencer: () => Promise; - getRecordsForDetector: () => Promise; - getRecords: () => Promise; - getRecordsForCriteria: () => Promise; - getMetricData: () => Promise; - getEventRateData: ( - index: string, - query: any, - timeFieldName: string, - earliestMs: number, - latestMs: number, - interval: string | number - ) => Promise; - getEventDistributionData: () => Promise; - getModelPlotOutput: ( - jobId: string, - detectorIndex: number, - criteriaFields: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - aggType: { - min: string; - max: string; - } - ) => Promise; - getRecordMaxScoreByTime: () => Promise; -} - -export const mlResultsService: MlResultsService; diff --git a/x-pack/legacy/plugins/ml/public/services/results_service.js b/x-pack/legacy/plugins/ml/public/services/results_service.js deleted file mode 100644 index 640600159084d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/services/results_service.js +++ /dev/null @@ -1,1861 +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. - */ - - - -// Service for carrying out Elasticsearch queries to obtain data for the -// Ml Results dashboards. -import _ from 'lodash'; -// import d3 from 'd3'; - -import { ML_MEDIAN_PERCENTS } from '../../common/util/job_utils'; -import { escapeForElasticsearchQuery } from '../util/string_utils'; -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; - -import { ml } from '../services/ml_api_service'; - -// Obtains the maximum bucket anomaly scores by job ID and time. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a results property, with a key for job -// which has results for the specified time range. -function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:bucket', - analyze_wildcard: false - } - }, { - bool: { - must: boolCriteria - } - }] - } - }, - aggs: { - jobId: { - terms: { - field: 'job_id', - size: maxResults !== undefined ? maxResults : 5, - order: { - anomalyScore: 'desc' - } - }, - aggs: { - anomalyScore: { - max: { - field: 'anomaly_score' - } - }, - byTime: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1, - extended_bounds: { - min: earliestMs, - max: latestMs - } - }, - aggs: { - anomalyScore: { - max: { - field: 'anomaly_score' - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const dataByJobId = _.get(resp, ['aggregations', 'jobId', 'buckets'], []); - _.each(dataByJobId, (dataForJob) => { - const jobId = dataForJob.key; - - const resultsForTime = {}; - - const dataByTime = _.get(dataForJob, ['byTime', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - const value = _.get(dataForTime, ['anomalyScore', 'value']); - if (value !== undefined) { - const time = dataForTime.key; - resultsForTime[time] = _.get(dataForTime, ['anomalyScore', 'value']); - } - }); - obj.results[jobId] = resultsForTime; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Obtains a list of scheduled events by job ID and time. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a events property, which will only -// contains keys for jobs which have scheduled events for the specified time range. -function getScheduledEventsByBucket( - jobIds, - earliestMs, - latestMs, - interval, - maxJobs, - maxEvents) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - events: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - exists: { field: 'scheduled_events' } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:bucket', - analyze_wildcard: false - } - }, { - bool: { - must: boolCriteria - } - }] - } - }, - aggs: { - jobs: { - terms: { - field: 'job_id', - min_doc_count: 1, - size: maxJobs - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 - }, - aggs: { - events: { - terms: { - field: 'scheduled_events', - size: maxEvents - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); - _.each(dataByJobId, (dataForJob) => { - const jobId = dataForJob.key; - const resultsForTime = {}; - const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - const time = dataForTime.key; - const events = _.get(dataForTime, ['events', 'buckets']); - resultsForTime[time] = _.map(events, 'key'); - }); - obj.events[jobId] = resultsForTime; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s). -// Pass an empty array or ['*'] to search over all job IDs. -// An optional array of influencers may be supplied, with each object in the array having 'fieldName' -// and 'fieldValue' properties, to limit data to the supplied list of influencers. -// Returned response contains an influencers property, with a key for each of the influencer field names, -// whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencers( - jobIds, - earliestMs, - latestMs, - maxFieldValues = 10, - influencers = [], - influencersFilterQuery) { - return new Promise((resolve, reject) => { - const obj = { success: true, influencers: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - influencer_score: { - gt: 0 - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - // Add a should query to filter for each of the specified influencers. - if (influencers.length > 0) { - boolCriteria.push({ - bool: { - should: influencers.map((influencer) => { - return { - bool: { - must: [ - { term: { influencer_field_name: influencer.fieldName } }, - { term: { influencer_field_value: influencer.fieldValue } } - ] - } - }; - }), - minimum_should_match: 1, - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:influencer', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - aggs: { - influencerFieldNames: { - terms: { - field: 'influencer_field_name', - size: 5, - order: { - maxAnomalyScore: 'desc' - } - }, - aggs: { - maxAnomalyScore: { - max: { - field: 'influencer_score' - } - }, - influencerFieldValues: { - terms: { - field: 'influencer_field_value', - size: maxFieldValues, - order: { - maxAnomalyScore: 'desc' - } - }, - aggs: { - maxAnomalyScore: { - max: { - field: 'influencer_score' - } - }, - sumAnomalyScore: { - sum: { - field: 'influencer_score' - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const fieldNameBuckets = _.get(resp, ['aggregations', 'influencerFieldNames', 'buckets'], []); - _.each(fieldNameBuckets, (nameBucket) => { - const fieldName = nameBucket.key; - const fieldValues = []; - - const fieldValueBuckets = _.get(nameBucket, ['influencerFieldValues', 'buckets'], []); - _.each(fieldValueBuckets, (valueBucket) => { - const fieldValueResult = { - influencerFieldValue: valueBucket.key, - maxAnomalyScore: valueBucket.maxAnomalyScore.value, - sumAnomalyScore: valueBucket.sumAnomalyScore.value - }; - fieldValues.push(fieldValueResult); - }); - - obj.influencers[fieldName] = fieldValues; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Obtains the top influencer field values, by maximum anomaly score, for a -// particular index, field name and job ID(s). -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a results property, which is an array of objects -// containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [{ - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(influencerFieldName)}`, - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - aggs: { - influencerFieldValues: { - terms: { - field: 'influencer_field_value', - size: maxResults !== undefined ? maxResults : 2, - order: { - maxAnomalyScore: 'desc' - } - }, - aggs: { - maxAnomalyScore: { - max: { - field: 'influencer_score' - } - }, - sumAnomalyScore: { - sum: { - field: 'influencer_score' - } - } - } - } - } - } - }) - .then((resp) => { - const buckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []); - _.each(buckets, (bucket) => { - const result = { - influencerFieldValue: bucket.key, - maxAnomalyScore: bucket.maxAnomalyScore.value, - sumAnomalyScore: bucket.sumAnomalyScore.value }; - obj.results.push(result); - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Obtains the overall bucket scores for the specified job ID(s). -// Pass ['*'] to search over all job IDs. -// Returned response contains a results property as an object of max score by time. -function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - ml.overallBuckets({ - jobId: jobIds, - topN: topN, - bucketSpan: interval, - start: earliestMs, - end: latestMs - }) - .then(resp => { - const dataByTime = _.get(resp, ['overall_buckets'], []); - _.each(dataByTime, (dataForTime) => { - const value = _.get(dataForTime, ['overall_score']); - if (value !== undefined) { - obj.results[dataForTime.timestamp] = value; - } - }); - - resolve(obj); - }) - .catch(resp => { - reject(resp); - }); - }); -} - -// Obtains the maximum score by influencer_field_value and by time for the specified job ID(s) -// (pass an empty array or ['*'] to search over all job IDs), and specified influencer field -// values (pass an empty array to search over all field values). -// Returned response contains a results property with influencer field values keyed -// against max score by time. -function getInfluencerValueMaxScoreByTime( - jobIds, - influencerFieldName, - influencerFieldValues, - earliestMs, - latestMs, - interval, - maxResults, - influencersFilterQuery) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - influencer_score: { - gt: 0 - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += `job_id:${jobId}`; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - if (influencerFieldValues && influencerFieldValues.length > 0) { - let influencerFilterStr = ''; - _.each(influencerFieldValues, (value, i) => { - if (i > 0) { - influencerFilterStr += ' OR '; - } - if (value.trim().length > 0) { - influencerFilterStr += `influencer_field_value:${escapeForElasticsearchQuery(value)}`; - } else { - // Wrap whitespace influencer field values in quotes for the query_string query. - influencerFilterStr += `influencer_field_value:"${value}"`; - } - - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: influencerFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(influencerFieldName)}`, - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - aggs: { - influencerFieldValues: { - terms: { - field: 'influencer_field_value', - size: maxResults !== undefined ? maxResults : 10, - order: { - maxAnomalyScore: 'desc' - } - }, - aggs: { - maxAnomalyScore: { - max: { - field: 'influencer_score' - } - }, - byTime: { - date_histogram: { - field: 'timestamp', - interval, - min_doc_count: 1 - }, - aggs: { - maxAnomalyScore: { - max: { - field: 'influencer_score' - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const fieldValueBuckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []); - _.each(fieldValueBuckets, (valueBucket) => { - const fieldValue = valueBucket.key; - const fieldValues = {}; - - const timeBuckets = _.get(valueBucket, ['byTime', 'buckets'], []); - _.each(timeBuckets, (timeBucket) => { - const time = timeBucket.key; - const score = timeBucket.maxAnomalyScore.value; - fieldValues[time] = score; - }); - - obj.results[fieldValue] = fieldValues; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Queries Elasticsearch to obtain record level results containing the influencers -// for the specified job(s), record score threshold, and time range. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a records property, with each record containing -// only the fields job_id, detector_index, record_score and influencers. -function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the existence of the nested influencers field, time range, - // record score, plus any specified job IDs. - const boolCriteria = [ - { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - exists: { field: 'influencers' } - } - ] - } - } - } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: maxResults !== undefined ? maxResults : 100, - rest_total_hits_as_int: true, - body: { - _source: ['job_id', 'detector_index', 'influencers', 'record_score'], - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ], - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Queries Elasticsearch to obtain the record level results containing the specified influencer(s), -// for the specified job(s), time range, and record score threshold. -// influencers parameter must be an array, with each object in the array having 'fieldName' -// 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, -// so this returns record level results which have at least one of the influencers. -// Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - // Add a nested query to filter for each of the specified influencers. - if (influencers.length > 0) { - boolCriteria.push({ - bool: { - should: influencers.map((influencer) => { - return { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': influencer.fieldName - } - }, - { - match: { - 'influencers.influencer_field_values': influencer.fieldValue - } - } - ] - } - } - } - }; - }), - minimum_should_match: 1, - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: maxResults !== undefined ? maxResults : 100, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ] - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Queries Elasticsearch to obtain the record level results for the specified job and detector, -// time range, record score threshold, and whether to only return results containing influencers. -// An additional, optional influencer field name and value may also be provided. -function getRecordsForDetector( - jobId, - detectorIndex, - checkForInfluencers, - influencerFieldName, - influencerFieldValue, - threshold, - earliestMs, - latestMs, - maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - term: { job_id: jobId } - }, - { - term: { detector_index: detectorIndex } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - // Add a nested query to filter for the specified influencer field name and value. - if (influencerFieldName && influencerFieldValue) { - boolCriteria.push({ - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': influencerFieldName - } - }, - { - match: { - 'influencers.influencer_field_values': influencerFieldValue - } - } - ] - } - } - } - }); - } else { - if (checkForInfluencers === true) { - boolCriteria.push({ - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - exists: { field: 'influencers' } - } - ] - } - } - } - }); - } - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: maxResults !== undefined ? maxResults : 100, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ], - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Queries Elasticsearch to obtain all the record level results for the specified job(s), time range, -// and record score threshold. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a records property, which is an array of the matching results. -function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { - return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); -} - -// Queries Elasticsearch to obtain the record level results matching the given criteria, -// for the specified job(s), time range, and record score threshold. -// criteriaFields parameter must be an array, with each object in the array having 'fieldName' -// 'fieldValue' properties. -// Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForCriteria(jobIds, criteriaFields, threshold, earliestMs, latestMs, maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - boolCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - rest_total_hits_as_int: true, - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ], - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Queries Elasticsearch to obtain metric aggregation results. -// index can be a String, or String[], of index names to search. -// entityFields parameter must be an array, with each object in the array having 'fieldName' -// and 'fieldValue' properties. -// Extra query object can be supplied, or pass null if no additional query -// to that built from the supplied entity fields. -// Returned response contains a results property containing the requested aggregation. -function getMetricData( - index, - entityFields, - query, - metricFunction, // ES aggregation name - metricFieldName, - timeFieldName, - earliestMs, - latestMs, - interval) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, entity fields, - // plus any additional supplied query. - const mustCriteria = []; - const shouldCriteria = []; - - mustCriteria.push({ - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }); - - if (query) { - mustCriteria.push(query); - } - - _.each(entityFields, (entity) => { - if (entity.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [entity.fieldName]: entity.fieldValue - } - }); - - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - shouldCriteria.push({ - bool: { - must: [ - { - term: { - [entity.fieldName]: '' - } - } - ] - } - }); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: entity.fieldName } - } - ] - } - }); - } - - }); - - const body = { - query: { - bool: { - must: mustCriteria - } - }, - size: 0, - _source: { - excludes: [] - }, - aggs: { - byTime: { - date_histogram: { - field: timeFieldName, - interval: interval, - min_doc_count: 0 - } - - } - } - }; - - if (shouldCriteria.length > 0) { - body.query.bool.should = shouldCriteria; - body.query.bool.minimum_should_match = shouldCriteria.length / 2; - } - - if (metricFieldName !== undefined && metricFieldName !== '') { - body.aggs.byTime.aggs = {}; - - const metricAgg = { - [metricFunction]: { - field: metricFieldName - } - }; - - if (metricFunction === 'percentiles') { - metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; - } - body.aggs.byTime.aggs.metric = metricAgg; - } - - ml.esSearch({ - index, - body - }) - .then((resp) => { - const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - if (metricFunction === 'count') { - obj.results[dataForTime.key] = dataForTime.doc_count; - } else { - const value = _.get(dataForTime, ['metric', 'value']); - const values = _.get(dataForTime, ['metric', 'values']); - if (dataForTime.doc_count === 0) { - obj.results[dataForTime.key] = null; - } else if (value !== undefined) { - obj.results[dataForTime.key] = value; - } else if (values !== undefined) { - // Percentiles agg currently returns NaN rather than null when none of the docs in the - // bucket contain the field used in the aggregation - // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). - // Store as null, so values can be handled in the same manner downstream as other aggs - // (min, mean, max) which return null. - const medianValues = values[ML_MEDIAN_PERCENTS]; - obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; - } else { - obj.results[dataForTime.key] = null; - } - } - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Queries Elasticsearch to obtain event rate data i.e. the count -// of documents over time. -// index can be a String, or String[], of index names to search. -// Extra query object can be supplied, or pass null if no additional query. -// Returned response contains a results property, which is an object -// of document counts against time (epoch millis). -function getEventRateData( - index, - query, - timeFieldName, - earliestMs, - latestMs, - interval) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, entity fields, - // plus any additional supplied query. - const mustCriteria = [{ - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }]; - - if (query) { - mustCriteria.push(query); - } - - ml.esSearch({ - index, - rest_total_hits_as_int: true, - size: 0, - body: { - query: { - bool: { - must: mustCriteria - } - }, - _source: { - excludes: [] - }, - aggs: { - eventRate: { - date_histogram: { - field: timeFieldName, - interval: interval, - min_doc_count: 0, - extended_bounds: { - min: earliestMs, - max: latestMs, - } - } - } - } - } - }) - .then((resp) => { - const dataByTimeBucket = _.get(resp, ['aggregations', 'eventRate', 'buckets'], []); - _.each(dataByTimeBucket, (dataForTime) => { - const time = dataForTime.key; - obj.results[time] = dataForTime.doc_count; - }); - obj.total = resp.hits.total; - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Queries Elasticsearch to obtain event distribution i.e. the count -// of entities over time. -// index can be a String, or String[], of index names to search. -// Extra query object can be supplied, or pass null if no additional query. -// Returned response contains a results property, which is an object -// of document counts against time (epoch millis). -const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000; -const ENTITY_AGGREGATION_SIZE = 10; -const AGGREGATION_MIN_DOC_COUNT = 1; -const CARDINALITY_PRECISION_THRESHOLD = 100; -function getEventDistributionData( - index, - splitField, - filterField = null, - query, - metricFunction, // ES aggregation name - metricFieldName, - timeFieldName, - earliestMs, - latestMs, - interval) { - return new Promise((resolve, reject) => { - if (splitField === undefined) { - return resolve([]); - } - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, entity fields, - // plus any additional supplied query. - const mustCriteria = []; - - mustCriteria.push({ - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }); - - if (query) { - mustCriteria.push(query); - } - - if (filterField !== null) { - mustCriteria.push({ - term: { - [filterField.fieldName]: filterField.fieldValue - } - }); - } - - const body = { - query: { - // using function_score and random_score to get a random sample of documents. - // otherwise all documents would have the same score and the sampler aggregation - // would pick the first N documents instead of a random set. - function_score: { - query: { - bool: { - must: mustCriteria - } - }, - functions: [ - { - random_score: { - // static seed to get same randomized results on every request - seed: 10, - field: '_seq_no' - } - } - ] - } - }, - size: 0, - _source: { - excludes: [] - }, - aggs: { - sample: { - sampler: { - shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE - }, - aggs: { - byTime: { - date_histogram: { - field: timeFieldName, - interval: interval, - min_doc_count: AGGREGATION_MIN_DOC_COUNT - }, - aggs: { - entities: { - terms: { - field: splitField.fieldName, - size: ENTITY_AGGREGATION_SIZE, - min_doc_count: AGGREGATION_MIN_DOC_COUNT - } - } - } - } - } - } - } - }; - - if (metricFieldName !== undefined && metricFieldName !== '') { - body.aggs.sample.aggs.byTime.aggs.entities.aggs = {}; - - const metricAgg = { - [metricFunction]: { - field: metricFieldName - } - }; - - if (metricFunction === 'percentiles') { - metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; - } - - if (metricFunction === 'cardinality') { - metricAgg[metricFunction].precision_threshold = CARDINALITY_PRECISION_THRESHOLD; - } - body.aggs.sample.aggs.byTime.aggs.entities.aggs.metric = metricAgg; - } - - ml.esSearch({ - index, - body, - rest_total_hits_as_int: true, - }) - .then((resp) => { - // Because of the sampling, results of metricFunctions which use sum or count - // can be significantly skewed. Taking into account totalHits we calculate a - // a factor to normalize results for these metricFunctions. - const totalHits = _.get(resp, ['hits', 'total'], 0); - const successfulShards = _.get(resp, ['_shards', 'successful'], 0); - - let normalizeFactor = 1; - if (totalHits > (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE)) { - normalizeFactor = totalHits / (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE); - } - - const dataByTime = _.get(resp, ['aggregations', 'sample', 'byTime', 'buckets'], []); - const data = dataByTime.reduce((d, dataForTime) => { - const date = +dataForTime.key; - const entities = _.get(dataForTime, ['entities', 'buckets'], []); - entities.forEach((entity) => { - let value = (metricFunction === 'count') ? entity.doc_count : entity.metric.value; - - if ( - metricFunction === 'count' - || metricFunction === 'cardinality' - || metricFunction === 'sum' - ) { - value = value * normalizeFactor; - } - - d.push({ - date, - entity: entity.key, - value - }); - }); - return d; - }, []); - resolve(data); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -function getModelPlotOutput( - jobId, - detectorIndex, - criteriaFields, - earliestMs, - latestMs, - interval, - aggType) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // if an aggType object has been passed in, use it. - // otherwise default to min and max aggs for the upper and lower bounds - const modelAggs = (aggType === undefined) ? - { max: 'max', min: 'min' } : - { - max: aggType.max, - min: aggType.min - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID and time range. - const mustCriteria = [ - { - term: { job_id: jobId } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - } - ]; - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - // Add criteria for the detector index. Results from jobs created before 6.1 will not - // contain a detector_index field, so use a should criteria with a 'not exists' check. - const shouldCriteria = [ - { - term: { detector_index: detectorIndex } - }, - { - bool: { - must_not: [ - { - exists: { field: 'detector_index' } - } - ] - } - } - ]; - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:model_plot', - analyze_wildcard: true - } - }, { - bool: { - must: mustCriteria, - should: shouldCriteria, - minimum_should_match: 1 - } - }] - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 0 - }, - aggs: { - actual: { - avg: { - field: 'actual' - } - }, - modelUpper: { - [modelAggs.max]: { - field: 'model_upper' - } - }, - modelLower: { - [modelAggs.min]: { - field: 'model_lower' - } - } - } - } - } - } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - let modelUpper = _.get(dataForTime, ['modelUpper', 'value']); - let modelLower = _.get(dataForTime, ['modelLower', 'value']); - const actual = _.get(dataForTime, ['actual', 'value']); - - if (modelUpper === undefined || isFinite(modelUpper) === false) { - modelUpper = null; - } - if (modelLower === undefined || isFinite(modelLower) === false) { - modelLower = null; - } - - obj.results[time] = { - actual, - modelUpper, - modelLower - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -// Queries Elasticsearch to obtain the max record score over time for the specified job, -// criteria, time range, and aggregation interval. -// criteriaFields parameter must be an array, with each object in the array having 'fieldName' -// 'fieldValue' properties. -function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // Build the criteria to use in the bool filter part of the request. - const mustCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { term: { job_id: jobId } } - ]; - const shouldCriteria = []; - - _.each(criteriaFields, (criteria) => { - if (criteria.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - const emptyFieldCondition = { - bool: { - must: [ - { - term: { - } - } - ] - } - }; - emptyFieldCondition.bool.must[0].term[criteria.fieldName] = ''; - shouldCriteria.push(emptyFieldCondition); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: criteria.fieldName } - } - ] - } - }); - } - - }); - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:record', - analyze_wildcard: true - } - }, { - bool: { - must: mustCriteria - } - }] - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 - }, - aggs: { - recordScore: { - max: { - field: 'record_score' - } - } - } - } - } - } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - obj.results[time] = { - score: _.get(dataForTime, ['recordScore', 'value']), - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - -export const mlResultsService = { - getScoresByBucket, - getScheduledEventsByBucket, - getTopInfluencers, - getTopInfluencerValues, - getOverallBucketScores, - getInfluencerValueMaxScoreByTime, - getRecordInfluencers, - getRecordsForInfluencer, - getRecordsForDetector, - getRecords, - getRecordsForCriteria, - getMetricData, - getEventRateData, - getEventDistributionData, - getModelPlotOutput, - getRecordMaxScoreByTime -}; diff --git a/x-pack/legacy/plugins/ml/public/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/settings/breadcrumbs.ts deleted file mode 100644 index 2cdfa5bfcf4d0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/settings/breadcrumbs.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../breadcrumbs'; - -export function getSettingsBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS]; -} - -export function getCalendarManagementBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, - ]; -} - -export function getCreateCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, - ]; -} - -export function getEditCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, - ]; -} - -export function getFilterListsBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, - ]; -} - -export function getCreateFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, - ]; -} - -export function getEditFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/settings/calendars/edit/utils.js deleted file mode 100644 index ef7ea0c256296..0000000000000 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/utils.js +++ /dev/null @@ -1,97 +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 { ml } from '../../../services/ml_api_service'; -import { isJobIdValid } from '../../../../common/util/job_utils'; -import { i18n } from '@kbn/i18n'; - - -function getJobIds() { - return new Promise((resolve, reject) => { - ml.jobs.jobsSummary() - .then((resp) => { - resolve(resp.map((job) => job.id)); - }) - .catch((err) => { - const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithFetchingJobSummariesErrorMessage', { - defaultMessage: 'Error fetching job summaries: {err}', - values: { err } - }); - console.log(errorMessage); - reject(errorMessage); - }); - }); -} - -function getGroupIds() { - return new Promise((resolve, reject) => { - ml.jobs.groups() - .then((resp) => { - resolve(resp.map((group) => group.id)); - }) - .catch((err) => { - const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingGroupsErrorMessage', { - defaultMessage: 'Error loading groups: {err}', - values: { err } - }); - console.log(errorMessage); - reject(errorMessage); - }); - }); -} - -function getCalendars() { - return new Promise((resolve, reject) => { - ml.calendars() - .then((resp) => { - resolve(resp); - }) - .catch((err) => { - const errorMessage = i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingCalendarsErrorMessage', { - defaultMessage: 'Error loading calendars: {err}', - values: { err } - }); - console.log(errorMessage); - reject(errorMessage); - }); - }); -} - -export function getCalendarSettingsData() { - return new Promise(async (resolve, reject) => { - try { - const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]); - - const formattedData = { - jobIds: data[0], - groupIds: data[1], - calendars: data[2] - }; - resolve(formattedData); - } catch (error) { - console.log(error); - reject(error); - } - }); -} - -export function validateCalendarId(calendarId) { - let valid = true; - - if (calendarId === '' || calendarId === undefined) { - valid = false; - } else if (isJobIdValid(calendarId) === false) { - valid = false; - } - - return valid; -} - -export function generateTempId() { - return Math.random().toString(36).substr(2, 9); -} diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/utils.js b/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/utils.js deleted file mode 100644 index 6303963ab3a14..0000000000000 --- a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/utils.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -import { isJobIdValid } from 'plugins/ml/../common/util/job_utils'; -import { ml } from 'plugins/ml/services/ml_api_service'; - -export function isValidFilterListId(id) { - // Filter List ID requires the same format as a Job ID, therefore isJobIdValid can be used - return (id !== undefined) && (id.length > 0) && isJobIdValid(id); -} - - -// Saves a filter list, running an update if the supplied loadedFilterList, holding the -// original filter list to which edits are being applied, is defined with a filter_id property. -export function saveFilterList(filterId, description, items, loadedFilterList) { - return new Promise((resolve, reject) => { - if (loadedFilterList === undefined || loadedFilterList.filter_id === undefined) { - // Create a new filter. - addFilterList(filterId, - description, - items - ) - .then((newFilter) => { - resolve(newFilter); - }) - .catch((error) => { - reject(error); - }); - } else { - // Edit to existing filter. - updateFilterList( - loadedFilterList, - description, - items) - .then((updatedFilter) => { - resolve(updatedFilter); - }) - .catch((error) => { - reject(error); - }); - - } - }); -} - -export function addFilterList(filterId, description, items) { - const filterWithIdExistsErrorMessage = i18n.translate('xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage', { - defaultMessage: 'A filter with id {filterId} already exists', - values: { - filterId, - }, - }); - - return new Promise((resolve, reject) => { - - // First check the filterId isn't already in use by loading the current list of filters. - ml.filters.filtersStats() - .then((filterLists) => { - const savedFilterIds = filterLists.map(filterList => filterList.filter_id); - if (savedFilterIds.indexOf(filterId) === -1) { - // Save the new filter. - ml.filters.addFilter( - filterId, - description, - items - ) - .then((newFilter) => { - resolve(newFilter); - }) - .catch((error) => { - reject(error); - }); - } else { - toastNotifications.addDanger(filterWithIdExistsErrorMessage); - reject(new Error(filterWithIdExistsErrorMessage)); - } - }) - .catch((error) => { - reject(error); - }); - - }); - -} - -export function updateFilterList(loadedFilterList, description, items) { - - return new Promise((resolve, reject) => { - - // Get items added and removed from loaded filter. - const loadedItems = loadedFilterList.items; - const addItems = items.filter(item => (loadedItems.includes(item) === false)); - const removeItems = loadedItems.filter(item => (items.includes(item) === false)); - - ml.filters.updateFilter( - loadedFilterList.filter_id, - description, - addItems, - removeItems - ) - .then((updatedFilter) => { - resolve(updatedFilter); - }) - .catch((error) => { - reject(error); - }); - }); -} diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/breadcrumbs.js deleted file mode 100644 index fd32a7c4d04b1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/breadcrumbs.js +++ /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 { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; -import { i18n } from '@kbn/i18n'; - - -export function getSingleMetricViewerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { - defaultMessage: 'Single Metric Viewer' - }), - href: '' - } - - ]; -} - diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/index.js deleted file mode 100644 index 946312d08e9ce..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/index.js +++ /dev/null @@ -1,11 +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 './timeseriesexplorer_directive.js'; -import './timeseriesexplorer_route.js'; -import './timeseries_search_service.js'; -import '../components/job_selector'; -import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js deleted file mode 100644 index 520cce3c73260..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js +++ /dev/null @@ -1,152 +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 _ from 'lodash'; - -import { ml } from '../services/ml_api_service'; -import { isModelPlotEnabled } from '../../common/util/job_utils'; -import { buildConfigFromDetector } from '../util/chart_config_builder'; -import { mlResultsService } from '../services/results_service'; - -function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, interval) { - if (isModelPlotEnabled(job, detectorIndex, entityFields)) { - // Extract the partition, by, over fields on which to filter. - const criteriaFields = []; - const detector = job.analysis_config.detectors[detectorIndex]; - if (_.has(detector, 'partition_field_name')) { - const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); - if (partitionEntity !== undefined) { - criteriaFields.push( - { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, - { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); - } - } - - if (_.has(detector, 'over_field_name')) { - const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); - if (overEntity !== undefined) { - criteriaFields.push( - { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, - { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); - } - } - - if (_.has(detector, 'by_field_name')) { - const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); - if (byEntity !== undefined) { - criteriaFields.push( - { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, - { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); - } - } - - return mlResultsService.getModelPlotOutput( - job.job_id, - detectorIndex, - criteriaFields, - earliestMs, - latestMs, - interval - ); - } else { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - const chartConfig = buildConfigFromDetector(job, detectorIndex); - - mlResultsService.getMetricData( - chartConfig.datafeedConfig.indices, - entityFields, - chartConfig.datafeedConfig.query, - chartConfig.metricFunction, - chartConfig.metricFieldName, - chartConfig.timeField, - earliestMs, - latestMs, - interval - ) - .then((resp) => { - _.each(resp.results, (value, time) => { - obj.results[time] = { - 'actual': value - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - - }); - } -} - -// Builds chart detail information (charting function description and entity counts) used -// in the title area of the time series chart. -// Queries Elasticsearch if necessary to obtain the distinct count of entities -// for which data is being plotted. -function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: { functionLabel: '', entityData: { entities: [] } } }; - - const chartConfig = buildConfigFromDetector(job, detectorIndex); - let functionLabel = chartConfig.metricFunction; - if (chartConfig.metricFieldName !== undefined) { - functionLabel += ' '; - functionLabel += chartConfig.metricFieldName; - } - obj.results.functionLabel = functionLabel; - - const blankEntityFields = _.filter(entityFields, (entity) => { - return entity.fieldValue.length === 0; - }); - - // Look to see if any of the entity fields have defined values - // (i.e. blank input), and if so obtain the cardinality. - if (blankEntityFields.length === 0) { - obj.results.entityData.count = 1; - obj.results.entityData.entities = entityFields; - resolve(obj); - } else { - const entityFieldNames = _.map(blankEntityFields, 'fieldName'); - ml.getCardinalityOfFields({ - index: chartConfig.datafeedConfig.indices, - fieldNames: entityFieldNames, - query: chartConfig.datafeedConfig.query, - timeFieldName: chartConfig.timeField, - earliestMs, - latestMs - }) - .then((results) => { - _.each(blankEntityFields, (field) => { - // results will not contain keys for non-aggregatable fields, - // so store as 0 to indicate over all field values. - obj.results.entityData.entities.push({ - fieldName: field.fieldName, - cardinality: _.get(results, field.fieldName, 0) - }); - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - } - - }); -} - -export const mlTimeSeriesSearchService = { - getMetricData, - getChartDetails -}; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_constants.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_constants.js deleted file mode 100644 index 52590bb6824c1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_constants.js +++ /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. - */ - -/* - * Contains values for ML time series explorer. - */ - - -export const APP_STATE_ACTION = { - CLEAR: 'CLEAR', - GET_DETECTOR_INDEX: 'GET_DETECTOR_INDEX', - SET_DETECTOR_INDEX: 'SET_DETECTOR_INDEX', - GET_ENTITIES: 'GET_ENTITIES', - SET_ENTITIES: 'SET_ENTITIES', - GET_FORECAST_ID: 'GET_FORECAST_ID', - SET_FORECAST_ID: 'SET_FORECAST_ID', - GET_ZOOM: 'GET_ZOOM', - SET_ZOOM: 'SET_ZOOM', - UNSET_ZOOM: 'UNSET_ZOOM', -}; - -export const CHARTS_POINT_TARGET = 500; - -// Max number of scheduled events displayed per bucket. -export const MAX_SCHEDULED_EVENTS = 10; - -export const TIME_FIELD_NAME = 'timestamp'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js deleted file mode 100644 index fb741702841ed..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js +++ /dev/null @@ -1,574 +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. - */ - - - -/* - * Contains a number of utility functions used for processing - * the data for exploring a time series in the Single Metric - * Viewer dashboard. - */ - -import _ from 'lodash'; -import moment from 'moment-timezone'; - -import { - ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../common/constants/search'; -import { - isTimeSeriesViewJob, - mlFunctionToESAggregation, -} from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; - -import { ml } from '../services/ml_api_service'; -import { mlForecastService } from '../services/forecast_service'; -import { mlResultsService } from '../services/results_service'; -import { TimeBuckets, getBoundsRoundedToInterval } from '../util/time_buckets'; - -import { mlTimeSeriesSearchService } from './timeseries_search_service'; - -import { - CHARTS_POINT_TARGET, - MAX_SCHEDULED_EVENTS, - TIME_FIELD_NAME, -} from './timeseriesexplorer_constants'; - -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - -// create new job objects based on standard job config objects -// new job objects just contain job id, bucket span in seconds and a selected flag. -// only time series view jobs are allowed -export function createTimeSeriesJobData(jobs) { - const singleTimeSeriesJobs = jobs.filter(isTimeSeriesViewJob); - return singleTimeSeriesJobs.map(job => { - const bucketSpan = parseInterval(job.analysis_config.bucket_span); - return { - id: job.job_id, - selected: false, - bucketSpanSeconds: bucketSpan.asSeconds() - }; - }); -} - -// Return dataset in format used by the single metric chart. -// i.e. array of Objects with keys date (JavaScript date) and value, -// plus lower and upper keys if model plot is enabled for the series. -export function processMetricPlotResults(metricPlotData, modelPlotEnabled) { - const metricPlotChartData = []; - if (modelPlotEnabled === true) { - _.each(metricPlotData, (dataForTime, time) => { - metricPlotChartData.push({ - date: new Date(+time), - lower: dataForTime.modelLower, - value: dataForTime.actual, - upper: dataForTime.modelUpper - }); - }); - } else { - _.each(metricPlotData, (dataForTime, time) => { - metricPlotChartData.push({ - date: new Date(+time), - value: dataForTime.actual - }); - }); - } - - return metricPlotChartData; -} - -// Returns forecast dataset in format used by the single metric chart. -// i.e. array of Objects with keys date (JavaScript date), isForecast, -// value, lower and upper keys. -export function processForecastResults(forecastData) { - const forecastPlotChartData = []; - _.each(forecastData, (dataForTime, time) => { - forecastPlotChartData.push({ - date: new Date(+time), - isForecast: true, - lower: dataForTime.forecastLower, - value: dataForTime.prediction, - upper: dataForTime.forecastUpper - }); - }); - - return forecastPlotChartData; -} - -// Return dataset in format used by the swimlane. -// i.e. array of Objects with keys date (JavaScript date) and score. -export function processRecordScoreResults(scoreData) { - const bucketScoreData = []; - _.each(scoreData, (dataForTime, time) => { - bucketScoreData.push( - { - date: new Date(+time), - score: dataForTime.score, - }); - }); - - return bucketScoreData; -} - -// Uses data from the list of anomaly records to add anomalyScore, -// function, actual and typical properties, plus causes and multi-bucket -// info if applicable, to the chartData entries for anomalous buckets. -export function processDataForFocusAnomalies( - chartData, - anomalyRecords, - aggregationInterval, - modelPlotEnabled) { - - const timesToAddPointsFor = []; - - // Iterate through the anomaly records making sure we have chart points for each anomaly. - const intervalMs = aggregationInterval.asMilliseconds(); - let lastChartDataPointTime = undefined; - if (chartData !== undefined && chartData.length > 0) { - lastChartDataPointTime = chartData[chartData.length - 1].date.getTime(); - } - anomalyRecords.forEach((record) => { - const recordTime = record[TIME_FIELD_NAME]; - const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); - if (chartPoint === undefined) { - const timeToAdd = (Math.floor(recordTime / intervalMs)) * intervalMs; - if (timesToAddPointsFor.indexOf(timeToAdd) === -1 && timeToAdd !== lastChartDataPointTime) { - timesToAddPointsFor.push(timeToAdd); - } - } - }); - - timesToAddPointsFor.sort((a, b) => a - b); - - timesToAddPointsFor.forEach((time) => { - const pointToAdd = { - date: new Date(time), - value: null - }; - - if (modelPlotEnabled === true) { - pointToAdd.upper = null; - pointToAdd.lower = null; - } - chartData.push(pointToAdd); - }); - - // Iterate through the anomaly records adding the - // various properties required for display. - anomalyRecords.forEach((record) => { - - // Look for a chart point with the same time as the record. - // If none found, find closest time in chartData set. - const recordTime = record[TIME_FIELD_NAME]; - const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); - if (chartPoint !== undefined) { - // If chart aggregation interval > bucket span, there may be more than - // one anomaly record in the interval, so use the properties from - // the record with the highest anomalyScore. - const recordScore = record.record_score; - const pointScore = chartPoint.anomalyScore; - if (pointScore === undefined || pointScore < recordScore) { - chartPoint.anomalyScore = recordScore; - chartPoint.function = record.function; - - if (_.has(record, 'actual')) { - chartPoint.actual = record.actual; - chartPoint.typical = record.typical; - } else { - const causes = _.get(record, 'causes', []); - if (causes.length > 0) { - chartPoint.byFieldName = record.by_field_name; - chartPoint.numberOfCauses = causes.length; - if (causes.length === 1) { - // If only a single cause, copy actual and typical values to the top level. - const cause = _.first(record.causes); - chartPoint.actual = cause.actual; - chartPoint.typical = cause.typical; - } - } - } - - if (_.has(record, 'multi_bucket_impact')) { - chartPoint.multiBucketImpact = record.multi_bucket_impact; - } - } - } - - }); - - return chartData; -} - -// Adds a scheduledEvents property to any points in the chart data set -// which correspond to times of scheduled events for the job. -export function processScheduledEventsForChart(chartData, scheduledEvents) { - if (scheduledEvents !== undefined) { - _.each(scheduledEvents, (events, time) => { - const chartPoint = findNearestChartPointToTime(chartData, time); - if (chartPoint !== undefined) { - // Note if the scheduled event coincides with an absence of the underlying metric data, - // we don't worry about plotting the event. - chartPoint.scheduledEvents = events; - } - }); - } - - return chartData; -} - -// Finds the chart point which is closest in time to the specified time. -export function findNearestChartPointToTime(chartData, time) { - let chartPoint; - if(chartData === undefined) { - return chartPoint; - } - - for (let i = 0; i < chartData.length; i++) { - if (chartData[i].date.getTime() === time) { - chartPoint = chartData[i]; - break; - } - } - - if (chartPoint === undefined) { - // Find nearest point in time. - // loop through line items until the date is greater than bucketTime - // grab the current and previous items and compare the time differences - let foundItem; - for (let i = 0; i < chartData.length; i++) { - const itemTime = chartData[i].date.getTime(); - if (itemTime > time) { - const item = chartData[i]; - const previousItem = chartData[i - 1]; - - const diff1 = Math.abs(time - previousItem.date.getTime()); - const diff2 = Math.abs(time - itemTime); - - // foundItem should be the item with a date closest to bucketTime - if (previousItem === undefined || diff1 > diff2) { - foundItem = item; - } else { - foundItem = previousItem; - } - - break; - } - } - - chartPoint = foundItem; - } - - return chartPoint; -} - -// Finds the chart point which corresponds to an anomaly with the -// specified time. -export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregationInterval) { - let chartPoint; - if(chartData === undefined) { - return chartPoint; - } - - for (let i = 0; i < chartData.length; i++) { - if (chartData[i].date.getTime() === anomalyTime) { - chartPoint = chartData[i]; - break; - } - } - - if (chartPoint === undefined) { - // Find the time of the point which falls immediately before the - // time of the anomaly. This is the start of the chart 'bucket' - // which contains the anomalous bucket. - let foundItem; - const intervalMs = aggregationInterval.asMilliseconds(); - for (let i = 0; i < chartData.length; i++) { - const itemTime = chartData[i].date.getTime(); - if (anomalyTime - itemTime < intervalMs) { - foundItem = chartData[i]; - break; - } - } - - chartPoint = foundItem; - } - - return chartPoint; -} - -export const getFocusData = function ( - criteriaFields, - detectorIndex, - focusAggregationInterval, - forecastId, - modelPlotEnabled, - nonBlankEntities, - searchBounds, - selectedJob, -) { - return new Promise((resolve, reject) => { - // Counter to keep track of the queries to populate the chart. - let awaitingCount = 4; - - // This object is used to store the results of individual remote requests - // before we transform it into the final data and apply it to $scope. Otherwise - // we might trigger multiple $digest cycles and depending on how deep $watches - // listen for changes we could miss updates. - const refreshFocusData = {}; - - // finish() function, called after each data set has been loaded and processed. - // The last one to call it will trigger the page render. - function finish() { - awaitingCount--; - if (awaitingCount === 0) { - // Tell the results container directives to render the focus chart. - refreshFocusData.focusChartData = processDataForFocusAnomalies( - refreshFocusData.focusChartData, - refreshFocusData.anomalyRecords, - focusAggregationInterval, - modelPlotEnabled, - ); - - refreshFocusData.focusChartData = processScheduledEventsForChart( - refreshFocusData.focusChartData, - refreshFocusData.scheduledEvents); - - resolve(refreshFocusData); - } - } - - // Query 1 - load metric data across selected time range. - mlTimeSeriesSearchService.getMetricData( - selectedJob, - detectorIndex, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression - ).then((resp) => { - refreshFocusData.focusChartData = processMetricPlotResults(resp.results, modelPlotEnabled); - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting metric data from elasticsearch:', resp); - reject(); - }); - - // Query 2 - load all the records across selected time range for the chart anomaly markers. - mlResultsService.getRecordsForCriteria( - [selectedJob.job_id], - criteriaFields, - 0, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - // Sort in descending time order before storing in scope. - refreshFocusData.anomalyRecords = _.chain(resp.records) - .sortBy(record => record[TIME_FIELD_NAME]) - .reverse() - .value(); - finish(); - }); - - // Query 3 - load any scheduled events for the selected job. - mlResultsService.getScheduledEventsByBucket( - [selectedJob.job_id], - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - 1, - MAX_SCHEDULED_EVENTS - ).then((resp) => { - refreshFocusData.scheduledEvents = resp.events[selectedJob.job_id]; - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting scheduled events from elasticsearch:', resp); - reject(); - }); - - // Query 4 - load any annotations for the selected job. - if (mlAnnotationsEnabled) { - ml.annotations.getAnnotations({ - jobIds: [selectedJob.job_id], - earliestMs: searchBounds.min.valueOf(), - latestMs: searchBounds.max.valueOf(), - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { - refreshFocusData.focusAnnotationData = resp.annotations[selectedJob.job_id] - .sort((a, b) => { - return a.timestamp - b.timestamp; - }) - .map((d, i) => { - d.key = String.fromCharCode(65 + i); - return d; - }); - - finish(); - }).catch(() => { - // silent fail - refreshFocusData.focusAnnotationData = []; - finish(); - }); - } else { - finish(); - } - - // Plus query for forecast data if there is a forecastId stored in the appState. - if (forecastId !== undefined) { - awaitingCount++; - let aggType = undefined; - const detector = selectedJob.analysis_config.detectors[detectorIndex]; - const esAgg = mlFunctionToESAggregation(detector.function); - if (modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { - aggType = { avg: 'sum', max: 'sum', min: 'sum' }; - } - - mlForecastService.getForecastData( - selectedJob, - detectorIndex, - forecastId, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - aggType) - .then((resp) => { - refreshFocusData.focusForecastData = processForecastResults(resp.results); - refreshFocusData.showForecastCheckbox = (refreshFocusData.focusForecastData.length > 0); - finish(); - }).catch((resp) => { - console.log(`Time series explorer - error loading data for forecast ID ${forecastId}`, resp); - reject(); - }); - } - }); -}; - -export function calculateAggregationInterval( - bounds, - bucketsTarget, - jobs, - selectedJob, -) { - // Aggregation interval used in queries should be a function of the time span of the chart - // and the bucket span of the selected job(s). - const barTarget = (bucketsTarget !== undefined ? bucketsTarget : 100); - // Use a maxBars of 10% greater than the target. - const maxBars = Math.floor(1.1 * barTarget); - const buckets = new TimeBuckets(); - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(Math.floor(barTarget)); - buckets.setMaxBars(maxBars); - - // Ensure the aggregation interval is always a multiple of the bucket span to avoid strange - // behaviour such as adjacent chart buckets holding different numbers of job results. - const bucketSpanSeconds = _.find(jobs, { 'id': selectedJob.job_id }).bucketSpanSeconds; - let aggInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); - - // Set the interval back to the job bucket span if the auto interval is smaller. - const secs = aggInterval.asSeconds(); - if (secs < bucketSpanSeconds) { - buckets.setInterval(bucketSpanSeconds + 's'); - aggInterval = buckets.getInterval(); - } - - return aggInterval; -} - -export function calculateDefaultFocusRange( - autoZoomDuration, - contextAggregationInterval, - contextChartData, - contextForecastData, -) { - const isForecastData = contextForecastData !== undefined && contextForecastData.length > 0; - - const combinedData = (isForecastData === false) ? - contextChartData : contextChartData.concat(contextForecastData); - const earliestDataDate = _.first(combinedData).date; - const latestDataDate = _.last(combinedData).date; - - let rangeEarliestMs; - let rangeLatestMs; - - if (isForecastData === true) { - // Return a range centred on the start of the forecast range, depending - // on the time range of the forecast and data. - const earliestForecastDataDate = _.first(contextForecastData).date; - const latestForecastDataDate = _.last(contextForecastData).date; - - rangeLatestMs = Math.min(earliestForecastDataDate.getTime() + (autoZoomDuration / 2), latestForecastDataDate.getTime()); - rangeEarliestMs = Math.max(rangeLatestMs - autoZoomDuration, earliestDataDate.getTime()); - } else { - // Returns the range that shows the most recent data at bucket span granularity. - rangeLatestMs = latestDataDate.getTime() + contextAggregationInterval.asMilliseconds(); - rangeEarliestMs = Math.max(earliestDataDate.getTime(), rangeLatestMs - autoZoomDuration); - } - - return [new Date(rangeEarliestMs), new Date(rangeLatestMs)]; -} - -export function calculateInitialFocusRange(zoomState, contextAggregationInterval, timefilter) { - if (zoomState !== undefined) { - // Check that the zoom times are valid. - // zoomFrom must be at or after context chart search bounds earliest, - // zoomTo must be at or before context chart search bounds latest. - const zoomFrom = moment(zoomState.from, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - const zoomTo = moment(zoomState.to, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - const bounds = timefilter.getActiveBounds(); - const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, true); - const earliest = searchBounds.min; - const latest = searchBounds.max; - - if (zoomFrom.isValid() && zoomTo.isValid && - zoomTo.isAfter(zoomFrom) && - zoomFrom.isBetween(earliest, latest, null, '[]') && - zoomTo.isBetween(earliest, latest, null, '[]')) { - return [zoomFrom.toDate(), zoomTo.toDate()]; - } - } - - return undefined; -} - -export function getAutoZoomDuration(jobs, selectedJob) { - // Calculate the 'auto' zoom duration which shows data at bucket span granularity. - // Get the minimum bucket span of selected jobs. - // TODO - only look at jobs for which data has been returned? - const bucketSpanSeconds = _.find(jobs, { 'id': selectedJob.job_id }).bucketSpanSeconds; - - // In most cases the duration can be obtained by simply multiplying the points target - // Check that this duration returns the bucket span when run back through the - // TimeBucket interval calculation. - let autoZoomDuration = (bucketSpanSeconds * 1000) * (CHARTS_POINT_TARGET - 1); - - // Use a maxBars of 10% greater than the target. - const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET); - const buckets = new TimeBuckets(); - buckets.setInterval('auto'); - buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET)); - buckets.setMaxBars(maxBars); - - // Set bounds from 'now' for testing the auto zoom duration. - const nowMs = new Date().getTime(); - const max = moment(nowMs); - const min = moment(nowMs - autoZoomDuration); - buckets.setBounds({ min, max }); - - const calculatedInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); - const calculatedIntervalSecs = calculatedInterval.asSeconds(); - if (calculatedIntervalSecs !== bucketSpanSeconds) { - // If we haven't got the span back, which may occur depending on the 'auto' ranges - // used in TimeBuckets and the bucket span of the job, then multiply by the ratio - // of the bucket span to the calculated interval. - autoZoomDuration = autoZoomDuration * (bucketSpanSeconds / calculatedIntervalSecs); - } - - return autoZoomDuration; -} diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/chart_utils.js b/x-pack/legacy/plugins/ml/public/util/__tests__/chart_utils.js deleted file mode 100644 index a6c8a9ed1ec17..0000000000000 --- a/x-pack/legacy/plugins/ml/public/util/__tests__/chart_utils.js +++ /dev/null @@ -1,284 +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 $ from 'jquery'; -import d3 from 'd3'; -import expect from '@kbn/expect'; -import { - chartLimits, - filterAxisLabels, - getChartType, - numTicks, - showMultiBucketAnomalyMarker, - showMultiBucketAnomalyTooltip, -} from '../chart_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; -import { CHART_TYPE } from '../../explorer/explorer_constants'; - -describe('ML - chart utils', () => { - - describe('chartLimits', () => { - - it('returns NaN when called without data', () => { - const limits = chartLimits(); - expect(limits.min).to.be.NaN; - expect(limits.max).to.be.NaN; - }); - - it('returns {max: 625736376, min: 201039318} for some test data', () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1, - actual: [228243469], typical: [133107.7703441773] - }, - { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: 625736376, anomalyScore: 97.32085, numberOfCauses: 1, - actual: [625736376], typical: [132830.424736973] - }, - { - date: new Date('2017-02-23T13:00:00.000Z'), - value: 201039318, anomalyScore: 59.83488, numberOfCauses: 1, - actual: [201039318], typical: [132739.5267403542] - } - ]; - - const limits = chartLimits(data); - - // {max: 625736376, min: 201039318} - expect(limits.min).to.be(201039318); - expect(limits.max).to.be(625736376); - }); - - it('adds 5% padding when min/max are the same, e.g. when there\'s only one data point', () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 100, anomalyScore: 50, numberOfCauses: 1, - actual: [100], typical: [100] - } - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(95); - expect(limits.max).to.be(105); - }); - - it('returns minimum of 0 when data includes an anomaly for missing data', () => { - const data = [ - { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: null, anomalyScore: 97.32085, - actual: [0], typical: [22.2] - }, - { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, - { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, - { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 } - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(0); - expect(limits.max).to.be(24.4); - }); - - }); - - describe('filterAxisLabels', () => { - - it('throws an error when called without arguments', () => { - expect(() => filterAxisLabels()).to.throwError(); - }); - - it('filters axis labels', () => { - // this provides a dummy structure of axis labels. - // the first one should always be filtered because it overflows on the - // left side of the axis. the last one should be filtered based on the - // given width parameter when doing the test calls. - $('body').append(` - - - - 06:00 - - - 12:00 - - - 18:00 - - - 00:00 - - - - `); - - const selector = '#filterAxisLabels .x.axis'; - - // given this width, the last tick should not be removed - filterAxisLabels(d3.selectAll(selector), 1000); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(3); - - // given this width, the last tick should be removed - filterAxisLabels(d3.selectAll(selector), 790); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(2); - - // clean up - $('#filterAxisLabels').remove(); - }); - - }); - - describe('getChartType', () => { - - const singleMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [], - }; - - const multiMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [ - { - fieldName: 'airline', - fieldValue: 'AAL', - fieldType: 'partition', - } - ], - }; - - const populationConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'http.response.body.bytes', - entityFields: [ - { - fieldName: 'source.ip', - fieldValue: '10.11.12.13', - fieldType: 'over', - } - ], - }; - - const rareConfig = { - metricFunction: 'count', - functionDescription: 'rare', - entityFields: [ - { - fieldName: 'http.response.status_code', - fieldValue: '404', - fieldType: 'by', - } - ], - }; - - const varpModelPlotConfig = { - metricFunction: null, - functionDescription: 'varp', - fieldName: 'NetworkOut', - entityFields: [ - { - fieldName: 'instance', - fieldValue: 'i-ef74d410', - fieldType: 'over', - } - ], - }; - - const overScriptFieldModelPlotConfig = { - metricFunction: 'count', - functionDescription: 'count', - fieldName: 'highest_registered_domain', - entityFields: [ - { - fieldName: 'highest_registered_domain', - fieldValue: 'elastic.co', - fieldType: 'over', - } - ], - datafeedConfig: { - script_fields: { - highest_registered_domain: { - script: { - source: 'return domainSplit(doc[\'query\'].value, params).get(1);', - lang: 'painless' - }, - ignore_failure: false - } - } - } - }; - - it('returns single metric chart type as expected for configs', () => { - expect(getChartType(singleMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(multiMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(varpModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(overScriptFieldModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - - }); - - it('returns event distribution chart type as expected for configs', () => { - expect(getChartType(rareConfig)).to.be(CHART_TYPE.EVENT_DISTRIBUTION); - }); - - it('returns population distribution chart type as expected for configs', () => { - expect(getChartType(populationConfig)).to.be(CHART_TYPE.POPULATION_DISTRIBUTION); - }); - - }); - - describe('numTicks', () => { - - it('returns 10 for 1000', () => { - expect(numTicks(1000)).to.be(10); - }); - - }); - - describe('showMultiBucketAnomalyMarker', () => { - - it('returns true for points with multiBucketImpact at or above medium impact', () => { - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyMarker({})).to.be(false); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(false); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false); - }); - - }); - - describe('showMultiBucketAnomalyTooltip', () => { - - it('returns true for points with multiBucketImpact at or above low impact', () => { - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(true); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyTooltip({})).to.be(false); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false); - }); - - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/util/chart_utils.js deleted file mode 100644 index c73c89ad8c16c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/util/chart_utils.js +++ /dev/null @@ -1,407 +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 d3 from 'd3'; -import { calculateTextWidth } from '../util/string_utils'; -import { MULTI_BUCKET_IMPACT } from '../../common/constants/multi_bucket_impact'; -import moment from 'moment'; -import rison from 'rison-node'; - -import chrome from 'ui/chrome'; -import { timefilter } from 'ui/timefilter'; - -import { CHART_TYPE } from '../explorer/explorer_constants'; - -export const LINE_CHART_ANOMALY_RADIUS = 7; -export const MULTI_BUCKET_SYMBOL_SIZE = 100; // In square pixels for use with d3 symbol.size -export const SCHEDULED_EVENT_SYMBOL_HEIGHT = 5; - -const MAX_LABEL_WIDTH = 100; - -export function chartLimits(data = []) { - const domain = d3.extent(data, (d) => { - let metricValue = d.value; - if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { - // If an anomaly coincides with a gap in the data, use the anomaly actual value. - metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; - } - return metricValue; - }); - const limits = { max: domain[1], min: domain[0] }; - - if (limits.max === limits.min) { - limits.max = d3.max(data, (d) => { - if (d.typical) { - return Math.max(d.value, d.typical); - } else { - // If analysis with by and over field, and more than one cause, - // there will be no actual and typical value. - // TODO - produce a better visual for population analyses. - return d.value; - } - }); - limits.min = d3.min(data, (d) => { - if (d.typical) { - return Math.min(d.value, d.typical); - } else { - // If analysis with by and over field, and more than one cause, - // there will be no actual and typical value. - // TODO - produce a better visual for population analyses. - return d.value; - } - }); - } - - // add padding of 5% of the difference between max and min - // if we ended up with the same value for both of them - if (limits.max === limits.min) { - const padding = limits.max * 0.05; - limits.max += padding; - limits.min -= padding; - } - - return limits; -} - -export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, radius = 1.5) { - // We need to do this because when creating a line for a chart which has data gaps, - // if there are single datapoints without any valid data before and after them, - // the lines created by using d3...defined() do not contain these data points. - // So this function adds additional circle elements to display the single - // datapoints in additional to the line created for the chart. - - // first reduce the dataset to data points - // where the previous and next one don't contain any data - const dotsData = data.reduce((p, c, i) => { - const previous = data[i - 1]; - const next = data[i + 1]; - if ( - (typeof previous === 'undefined' || (previous && previous.value === null)) && - c.value !== null && - (typeof next === 'undefined' || (next && next.value === null)) - ) { - p.push(c); - } - return p; - }, []); - - // check if `g.values-dots` already exists, if not create it - // in both cases assign the element to `dotGroup` - const dotGroup = (lineChartGroup.select('.values-dots').empty()) - ? lineChartGroup.append('g').classed('values-dots', true) - : lineChartGroup.select('.values-dots'); - - // use d3's enter/update/exit pattern to render the dots - const dots = dotGroup.selectAll('circle').data(dotsData); - - dots.enter().append('circle') - .attr('r', radius); - - dots - .attr('cx', lineChartValuesLine.x()) - .attr('cy', lineChartValuesLine.y()); - - dots.exit().remove(); -} - -// this replicates Kibana's filterAxisLabels() behavior -// which can be found in ui/vislib/lib/axis/axis_labels.js -// axis labels which overflow the chart's boundaries will be removed -export function filterAxisLabels(selection, chartWidth) { - if (selection === undefined || selection.selectAll === undefined) { - throw new Error('Missing selection parameter'); - } - - selection.selectAll('.tick text') - // don't refactor this to an arrow function because - // we depend on using `this` here. - .text(function () { - const parent = d3.select(this.parentNode); - const labelWidth = parent.node().getBBox().width; - const labelXPos = d3.transform(parent.attr('transform')).translate[0]; - const minThreshold = labelXPos - (labelWidth / 2); - const maxThreshold = labelXPos + (labelWidth / 2); - if (minThreshold >= 0 && maxThreshold <= chartWidth) { - return this.textContent; - } else { - parent.remove(); - } - }); -} - -// feature flags for chart types -const EVENT_DISTRIBUTION_ENABLED = true; -const POPULATION_DISTRIBUTION_ENABLED = true; - -// get the chart type based on its configuration -export function getChartType(config) { - let chartType = CHART_TYPE.SINGLE_METRIC; - if ( - EVENT_DISTRIBUTION_ENABLED && - config.functionDescription === 'rare' && - (config.entityFields.some(f => f.fieldType === 'over') === false) - ) { - chartType = CHART_TYPE.EVENT_DISTRIBUTION; - } else if ( - POPULATION_DISTRIBUTION_ENABLED && - config.functionDescription !== 'rare' && - config.entityFields.some(f => f.fieldType === 'over') && - config.metricFunction !== null // Event distribution chart relies on the ML function mapping to an ES aggregation - ) { - chartType = CHART_TYPE.POPULATION_DISTRIBUTION; - } - - if (chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - // Check that the config does not use script fields defined in the datafeed config. - if (config.datafeedConfig !== undefined && config.datafeedConfig.script_fields !== undefined) { - const scriptFields = Object.keys(config.datafeedConfig.script_fields); - const checkFields = config.entityFields.map(entity => entity.fieldName); - if (config.metricFieldName) { - checkFields.push(config.metricFieldName); - } - const usesScriptFields = - (checkFields.find(fieldName => scriptFields.includes(fieldName)) !== undefined); - if (usesScriptFields === true) { - // Only single metric chart type supports query of model plot data. - chartType = CHART_TYPE.SINGLE_METRIC; - } - } - } - - return chartType; -} - -export function getExploreSeriesLink(series) { - // Open the Single Metric dashboard over the same overall bounds and - // zoomed in to the same time as the current chart. - const bounds = timefilter.getActiveBounds(); - const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z - const to = bounds.max.toISOString(); - - const zoomFrom = moment(series.plotEarliest).toISOString(); - const zoomTo = moment(series.plotLatest).toISOString(); - - // Pass the detector index and entity fields (i.e. by, over, partition fields) - // to identify the particular series to view. - // Initially pass them in the mlTimeSeriesExplorer part of the AppState. - // TODO - do we want to pass the entities via the filter? - const entityCondition = {}; - series.entityFields.forEach((entity) => { - entityCondition[entity.fieldName] = entity.fieldValue; - }); - - // Use rison to build the URL . - const _g = rison.encode({ - ml: { - jobIds: [series.jobId] - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0 - }, - time: { - from: from, - to: to, - mode: 'absolute' - } - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { - zoom: { - from: zoomFrom, - to: zoomTo - }, - detectorIndex: series.detectorIndex, - entities: entityCondition, - }, - query: { - query_string: { - analyze_wildcard: true, - query: '*' - } - } - }); - - return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; -} - -export function showMultiBucketAnomalyMarker(point) { - // TODO - test threshold with real use cases - return (point.multiBucketImpact !== undefined && - point.multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM); -} - -export function showMultiBucketAnomalyTooltip(point) { - // TODO - test threshold with real use cases - return (point.multiBucketImpact !== undefined && - point.multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW); -} - -export function numTicks(axisWidth) { - return axisWidth / MAX_LABEL_WIDTH; -} - -export function numTicksForDateFormat(axisWidth, dateFormat) { - // Allow 1.75 times the width of a formatted date per tick for padding. - const tickWidth = calculateTextWidth(moment().format(dateFormat), false); - return axisWidth / (1.75 * tickWidth); -} - -const TICK_DIRECTION = { - NEXT: 'next', - PREVIOUS: 'previous' -}; - -// Based on a fixed starting timestamp and an interval, get tick values within -// the bounds of earliest and latest. This is useful for the Anomaly Explorer Charts -// to align axis ticks with the gray area resembling the swimlane cell selection. -export function getTickValues(startTimeMs, tickInterval, earliest, latest) { - // A tickInterval equal or smaller than 0 would trigger a call stack exception, - // so we're trying to catch that before it happens. - if (tickInterval <= 0) { - throw Error('tickInterval must be larger than 0.'); - } - - const tickValues = [startTimeMs]; - - function addTicks(ts, operator) { - let newTick; - let addAnotherTick; - - switch (operator) { - case TICK_DIRECTION.PREVIOUS: - newTick = ts - tickInterval; - addAnotherTick = newTick >= earliest; - break; - case TICK_DIRECTION.NEXT: - newTick = ts + tickInterval; - addAnotherTick = newTick <= latest; - break; - } - - if (addAnotherTick) { - tickValues.push(newTick); - addTicks(newTick, operator); - } - } - - addTicks(startTimeMs, TICK_DIRECTION.PREVIOUS); - addTicks(startTimeMs, TICK_DIRECTION.NEXT); - - tickValues.sort(); - - return tickValues; -} - -const LABEL_WRAP_THRESHOLD = 60; - -// Checks if the string length of a chart label (detector description -// and entity fields) is above LABEL_WRAP_THRESHOLD. -export function isLabelLengthAboveThreshold({ detectorLabel, entityFields }) { - const labelLength = (detectorLabel.length + entityFields.map(d => `${d.fieldName} ${d.fieldValue}`).join(' ').length); - return (labelLength > LABEL_WRAP_THRESHOLD); -} - -// To get xTransform it would be nicer to use d3.transform, but that doesn't play well with JSDOM. -// So this uses a regex variant because we definitely want test coverage for the label removal. -// Once JSDOM supports SVGAnimatedTransformList we can use this simpler inline version: -// const xTransform = d3.transform(tick.attr('transform')).translate[0]; -export function getXTransform(t) { - const regexResult = /translate\(\s*([^\s,)]+)([ ,]([^\s,)]+))?\)/.exec(t); - if (Array.isArray(regexResult) && regexResult.length >= 2) { - return Number(regexResult[1]); - } - - // fall back to NaN if regex didn't return any results. - return NaN; -} - -// This removes overlapping x-axis labels by starting off from a specific label -// that is required/wanted to show up. The code then traverses to both sides along the axis -// and decides which labels to keep or remove. All vertical tick lines will be kept visible, -// but those which still have their text label will be emphasized using the ml-tick-emphasis class. -export function removeLabelOverlap(axis, startTimeMs, tickInterval, width) { - // Put emphasis on all tick lines, will again de-emphasize the - // ones where we remove the label in the next steps. - axis.selectAll('g.tick').select('line').classed('ml-tick-emphasis', true); - - function getNeighborTickFactory(operator) { - return function (ts) { - switch (operator) { - case TICK_DIRECTION.PREVIOUS: - return ts - tickInterval; - case TICK_DIRECTION.NEXT: - return ts + tickInterval; - } - }; - } - - function getTickDataFactory(operator) { - const getNeighborTick = getNeighborTickFactory(operator); - const fn = function (ts) { - const filteredTicks = axis.selectAll('.tick').filter(d => d === ts); - - if (filteredTicks.length === 0 || filteredTicks[0].length === 0) { - return false; - } - - const tick = d3.selectAll(filteredTicks[0]); - const textNode = tick.select('text').node(); - - if (textNode === null) { - return fn(getNeighborTick(ts)); - } - - const tickWidth = textNode.getBBox().width; - const padding = 15; - const xTransform = getXTransform(tick.attr('transform')); - const xMinOffset = xTransform - (tickWidth / 2 + padding); - const xMaxOffset = xTransform + (tickWidth / 2 + padding); - - return { - tick, - ts, - xMinOffset, - xMaxOffset - }; - }; - return fn; - } - - function checkTicks(ts, operator) { - const getTickData = getTickDataFactory(operator); - const currentTickData = getTickData(ts); - - if (currentTickData === false) { - return; - } - - const getNeighborTick = getNeighborTickFactory(operator); - const newTickData = getTickData(getNeighborTick(ts)); - - if (newTickData !== false) { - if ( - newTickData.xMinOffset < 0 || - newTickData.xMaxOffset > width || - (newTickData.xMaxOffset > currentTickData.xMinOffset && operator === TICK_DIRECTION.PREVIOUS) || - (newTickData.xMinOffset < currentTickData.xMaxOffset && operator === TICK_DIRECTION.NEXT) - ) { - newTickData.tick.select('text').remove(); - newTickData.tick.select('line').classed('ml-tick-emphasis', false); - checkTicks(currentTickData.ts, operator); - } else { - checkTicks(newTickData.ts, operator); - } - } - } - - checkTicks(startTimeMs, TICK_DIRECTION.PREVIOUS); - checkTicks(startTimeMs, TICK_DIRECTION.NEXT); -} diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 6bc98ba68f60b..7a9766f36a6ed 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { createMlTelemetry, getSavedObjectsClient, @@ -14,12 +15,11 @@ import { import { UsageInitialization } from '../../new_platform/plugin'; -export function makeMlUsageCollector({ - elasticsearchPlugin, - usage, - savedObjects, -}: UsageInitialization): void { - const mlUsageCollector = usage.collectorSet.makeUsageCollector({ +export function makeMlUsageCollector( + usageCollection: UsageCollectionSetup, + { elasticsearchPlugin, savedObjects }: UsageInitialization +): void { + const mlUsageCollector = usageCollection.makeUsageCollector({ type: 'ml', isReady: () => true, fetch: async (): Promise => { @@ -35,5 +35,6 @@ export function makeMlUsageCollector({ } }, }); - usage.collectorSet.register(mlUsageCollector); + + usageCollection.registerCollector(mlUsageCollector); } diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index b2b697a851703..b789121beebfc 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -10,6 +10,7 @@ import { ServerRoute } from 'hapi'; import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; @@ -68,12 +69,6 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; } export interface MlInitializerContext extends PluginInitializerContext { legacyConfig: KibanaConfig; @@ -84,6 +79,7 @@ export interface PluginsSetup { xpackMain: MlXpackMainPlugin; security: any; spaces: any; + usageCollection: UsageCollectionSetup; // TODO: this is temporary for `mirrorPluginStatus` ml: any; } @@ -98,12 +94,6 @@ export interface RouteInitialization { } export interface UsageInitialization { elasticsearchPlugin: ElasticsearchPlugin; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; savedObjects: SavedObjectsLegacyService; } @@ -201,10 +191,8 @@ export class Plugin { savedObjects: core.savedObjects, spacesPlugin: plugins.spaces, }; - const usageInitializationDeps: UsageInitialization = { elasticsearchPlugin: plugins.elasticsearch, - usage: core.usage, savedObjects: core.savedObjects, }; @@ -231,7 +219,7 @@ export class Plugin { fileDataVisualizerRoutes(extendedRouteInitializationDeps); initMlServerLog(logInitializationDeps); - makeMlUsageCollector(usageInitializationDeps); + makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps); } public stop() {} diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index 97046bfb7d5b4..79db8cb920ea3 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -56,9 +56,6 @@ export const monitoring = (kibana) => new kibana.Plugin({ throw `Unknown key '${key}'`; } }), - usage: { - collectorSet: server.usage.collectorSet - }, injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), getOSInfo: server.getOSInfo, @@ -70,11 +67,12 @@ export const monitoring = (kibana) => new kibana.Plugin({ _hapi: server, _kbnServer: this.kbnServer }; - + const { usageCollection } = server.newPlatform.setup.plugins; const plugins = { xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + usageCollection, }; new Plugin().setup(serverFacade, plugins); diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap index 12b82be333703..b52bdb7f553a6 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap @@ -78,6 +78,8 @@ exports[`SetupModeRenderer should render the flyout open 1`] = ` @@ -173,6 +175,8 @@ exports[`SetupModeRenderer should render with setup mode enabled 1`] = ` diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js index a07a26f64acff..dadb31f2cc83b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js @@ -10,7 +10,7 @@ import { updateSetupModeData, disableElasticsearchInternalCollection, toggleSetupMode, - setSetupModeMenuItem + setSetupModeMenuItem, } from '../../lib/setup_mode'; import { Flyout } from '../metricbeat_migration/flyout'; import { @@ -20,7 +20,7 @@ import { EuiFlexItem, EuiTextColor, EuiIcon, - EuiSpacer + EuiSpacer, } from '@elastic/eui'; import { findNewUuid } from './lib/find_new_uuid'; import { i18n } from '@kbn/i18n'; @@ -33,11 +33,11 @@ export class SetupModeRenderer extends React.Component { instance: null, newProduct: null, isSettingUpNew: false, - } + }; componentWillMount() { const { scope, injector } = this.props; - initSetupModeState(scope, injector, (_oldData) => { + initSetupModeState(scope, injector, _oldData => { const newState = { renderState: true }; const { productName } = this.props; if (!productName) { @@ -95,10 +95,9 @@ export class SetupModeRenderer extends React.Component { const uuids = Object.values(data.byUuid); if (uuids.length && !isSettingUpNew) { product = uuids[0]; - } - else { + } else { product = { - isNetNewUser: true + isNetNewUser: true, }; } } @@ -123,7 +122,7 @@ export class SetupModeRenderer extends React.Component { return ( - + @@ -134,9 +133,7 @@ export class SetupModeRenderer extends React.Component { id="xpack.monitoring.setupMode.description" defaultMessage="You are in setup mode. The ({flagIcon}) icon indicates configuration options." values={{ - flagIcon: ( - - ) + flagIcon: , }} /> @@ -146,9 +143,16 @@ export class SetupModeRenderer extends React.Component { - toggleSetupMode(false)}> + toggleSetupMode(false)} + > {i18n.translate('xpack.monitoring.setupMode.exit', { - defaultMessage: `Exit setup mode` + defaultMessage: `Exit setup mode`, })} @@ -173,8 +177,7 @@ export class SetupModeRenderer extends React.Component { if (setupModeState.data) { if (productName) { data = setupModeState.data[productName]; - } - else { + } else { data = setupModeState.data; } } @@ -189,11 +192,12 @@ export class SetupModeRenderer extends React.Component { productName, updateSetupModeData, shortcutToFinishMigration: () => this.shortcutToFinishMigration(), - openFlyout: (instance, isSettingUpNew) => this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), + openFlyout: (instance, isSettingUpNew) => + this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), closeFlyout: () => this.setState({ isFlyoutOpen: false }), }, flyoutComponent: this.getFlyout(data, meta), - bottomBarComponent: this.getBottomBar(setupModeState) + bottomBarComponent: this.getBottomBar(setupModeState), }); } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap new file mode 100644 index 0000000000000..2eaa25803c81e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EnterButton should render properly 1`] = ` +
+ + Enter setup mode + +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss new file mode 100644 index 0000000000000..a5ab07618f267 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss @@ -0,0 +1,6 @@ +.monSetupModeEnterButton__buttonWrapper { + position: absolute; + top: $euiSize; + left: $euiSizeM; + z-index: 1; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss new file mode 100644 index 0000000000000..b9c218fc4f39c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss @@ -0,0 +1 @@ +@import 'enter_button'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx new file mode 100644 index 0000000000000..1a8f15ce5f938 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { SetupModeEnterButton } from './enter_button'; + +describe('EnterButton', () => { + it('should render properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show a loading state', () => { + const component = shallow(); + + component.find('EuiButton').simulate('click'); + + expect(component.find('EuiButton').prop('isLoading')).toBe(true); + }); + + it('should call toggleSetupMode', () => { + const toggleSetupMode = jest.fn(); + const component = shallow( + + ); + + component.find('EuiButton').simulate('click'); + expect(toggleSetupMode).toHaveBeenCalledWith(true); + }); + + it('should not render if not enabled', () => { + const toggleSetupMode = jest.fn(); + const component = shallow( + + ); + expect(component.html()).toBe(null); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx new file mode 100644 index 0000000000000..8adcb635a6559 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface SetupModeEnterButtonProps { + enabled: boolean; + toggleSetupMode: (state: boolean) => void; +} + +export const SetupModeEnterButton: React.FC = ( + props: SetupModeEnterButtonProps +) => { + const [isLoading, setIsLoading] = React.useState(false); + + if (!props.enabled) { + return null; + } + + async function enterSetupMode() { + setIsLoading(true); + await props.toggleSetupMode(true); + setIsLoading(false); + } + + return ( +
+ + {i18n.translate('xpack.monitoring.setupMode.enter', { + defaultMessage: 'Enter setup mode', + })} + +
+ ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html b/x-pack/legacy/plugins/monitoring/public/directives/main/index.html index c989074b6de0a..bb34cf6f5bb63 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/legacy/plugins/monitoring/public/directives/main/index.html @@ -1,7 +1,7 @@
+
- + - + {{ monitoringMain.instance }} + class="euiTab" + >{{ monitoringMain.instance }}
+ ng-init="monitoringMain.dropdownLoadedHandler()" + >
diff --git a/x-pack/legacy/plugins/monitoring/public/index.scss b/x-pack/legacy/plugins/monitoring/public/index.scss index 8200cfef0ff3f..41bca7774a8b8 100644 --- a/x-pack/legacy/plugins/monitoring/public/index.scss +++ b/x-pack/legacy/plugins/monitoring/public/index.scss @@ -20,3 +20,4 @@ @import 'components/table/index'; @import 'components/logstash/pipeline_viewer/views/index'; @import 'components/elasticsearch/shard_allocation/index'; +@import 'components/setup_mode/index'; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js index 607edbd1e8709..239c6e3fd775a 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { render } from 'react-dom'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; function isOnPage(hash) { return contains(window.location.hash, hash); @@ -21,15 +24,15 @@ const angularState = { const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' - + ' This needs to be set by calling `initSetupModeState`.'; + throw 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.'; } }; const setupModeState = { enabled: false, data: null, - callbacks: [] + callbacks: [], }; export const getSetupModeState = () => setupModeState; @@ -55,26 +58,23 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) let url = '../api/monitoring/v1/setup/collection'; if (uuid) { url += `/node/${uuid}`; - } - else if (!fetchWithoutClusterUuid && clusterUuid) { + } else if (!fetchWithoutClusterUuid && clusterUuid) { url += `/cluster/${clusterUuid}`; - } - else { + } else { url += '/cluster'; } try { const response = await http.post(url, { ccs }); return response.data; - } - catch (err) { + } catch (err) { const Private = angularState.injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); } }; -const notifySetupModeDataChange = (oldData) => { +const notifySetupModeDataChange = oldData => { setupModeState.callbacks.forEach(cb => cb(oldData)); }; @@ -86,18 +86,21 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const isCloud = chrome.getInjected('isOnCloud'); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloud || !hasPermissions) { - const text = !hasPermissions - ? i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { - defaultMessage: 'You do not have the necessary permissions to do this.' - }) - : i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { - defaultMessage: 'This feature is not available on cloud.' + let text = null; + if (!hasPermissions) { + text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { + defaultMessage: 'You do not have the necessary permissions to do this.', }); + } else { + text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { + defaultMessage: 'This feature is not available on cloud.', + }); + } angularState.scope.$evalAsync(() => { toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { - defaultMessage: 'Setup mode is not available' + defaultMessage: 'Setup mode is not available', }), text, }); @@ -110,8 +113,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { const liveClusterUuid = get(data, '_meta.liveClusterUuid'); - const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})) - .filter(node => node.isPartiallyMigrated || node.isFullyMigrated); + const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( + node => node.isPartiallyMigrated || node.isFullyMigrated + ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); } @@ -128,8 +132,7 @@ export const disableElasticsearchInternalCollection = async () => { try { const response = await http.post(url); return response.data; - } - catch (err) { + } catch (err) { const Private = angularState.injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); @@ -160,23 +163,12 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const navItems = []; - if (!globalState.inSetupMode && !chrome.getInjected('isOnCloud')) { - navItems.push({ - id: 'enter', - label: i18n.translate('xpack.monitoring.setupMode.enter', { - defaultMessage: 'Enter Setup Mode' - }), - run: () => toggleSetupMode(true), - testId: 'enterSetupMode' - }); - } + const enabled = !globalState.inSetupMode && !chrome.getInjected('isOnCloud'); - angularState.scope.topNavMenu = [...navItems]; - // LOL angular - if (!angularState.scope.$$phase) { - angularState.scope.$apply(); - } + render( + , + document.getElementById('setupModeNav') + ); }; export const initSetupModeState = async ($scope, $injector, callback) => { @@ -195,7 +187,7 @@ export const isInSetupMode = async () => { return true; } - const $injector = angularState.injector || await chrome.dangerouslyGetActiveInjector(); + const $injector = angularState.injector || (await chrome.dangerouslyGetActiveInjector()); const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index 39ed049ab7492..1a9fdfeb920da 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -13,36 +13,40 @@ let setSetupModeMenuItem; jest.mock('./ajax_error_handler', () => ({ ajaxErrorHandlersProvider: err => { throw err; - } + }, +})); + +jest.mock('react-dom', () => ({ + render: jest.fn(), })); let data = {}; const injectorModulesMock = { globalState: { - save: jest.fn() + save: jest.fn(), }, Private: module => module, $http: { post: jest.fn().mockImplementation(() => { return { data }; - }) + }), }, $executor: { - run: jest.fn() - } + run: jest.fn(), + }, }; const angularStateMock = { injector: { get: module => { return injectorModulesMock[module] || {}; - } + }, }, scope: { $apply: fn => fn && fn(), - $evalAsync: fn => fn && fn() - } + $evalAsync: fn => fn && fn(), + }, }; // We are no longer waiting for setup mode data to be fetched when enabling @@ -66,11 +70,11 @@ function setModules() { describe('setup_mode', () => { beforeEach(async () => { jest.doMock('ui/chrome', () => ({ - getInjected: (key) => { + getInjected: key => { if (key === 'isOnCloud') { return false; } - } + }, })); setModules(); }); @@ -80,13 +84,14 @@ describe('setup_mode', () => { let error; try { toggleSetupMode(true); - } - catch (err) { + } catch (err) { error = err; } - expect(error).toEqual('Unable to interact with setup ' - + 'mode because the angular injector was not previously set. This needs to be ' - + 'set by calling `initSetupModeState`.'); + expect(error).toEqual( + 'Unable to interact with setup ' + + 'mode because the angular injector was not previously set. This needs to be ' + + 'set by calling `initSetupModeState`.' + ); }); it('should enable toggle mode', async () => { @@ -102,11 +107,11 @@ describe('setup_mode', () => { }); it('should set top nav config', async () => { + const render = require('react-dom').render; initSetupModeState(angularStateMock.scope, angularStateMock.injector); setSetupModeMenuItem(); - expect(angularStateMock.scope.topNavMenu.length).toBe(1); await toggleSetupMode(true); - expect(angularStateMock.scope.topNavMenu.length).toBe(0); + expect(render.mock.calls.length).toBe(2); }); }); @@ -115,32 +120,24 @@ describe('setup_mode', () => { data = {}; }); - it('should enable it through clicking top nav item', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - setSetupModeMenuItem(); - expect(injectorModulesMock.globalState.inSetupMode).toBe(false); - await angularStateMock.scope.topNavMenu[0].run(); - expect(injectorModulesMock.globalState.inSetupMode).toBe(true); - }); - - it('should not fetch data if on cloud', async (done) => { + it('should not fetch data if on cloud', async done => { const addDanger = jest.fn(); jest.doMock('ui/chrome', () => ({ - getInjected: (key) => { + getInjected: key => { if (key === 'isOnCloud') { return true; } - } + }, })); data = { _meta: { - hasPermissions: true - } + hasPermissions: true, + }, }; jest.doMock('ui/notify', () => ({ toastNotifications: { addDanger, - } + }, })); setModules(); initSetupModeState(angularStateMock.scope, angularStateMock.injector); @@ -150,23 +147,23 @@ describe('setup_mode', () => { expect(state.enabled).toBe(false); expect(addDanger).toHaveBeenCalledWith({ title: 'Setup mode is not available', - text: 'This feature is not available on cloud.' + text: 'This feature is not available on cloud.', }); done(); }); }); - it('should not fetch data if the user does not have sufficient permissions', async (done) => { + it('should not fetch data if the user does not have sufficient permissions', async done => { const addDanger = jest.fn(); jest.doMock('ui/notify', () => ({ toastNotifications: { addDanger, - } + }, })); data = { _meta: { - hasPermissions: false - } + hasPermissions: false, + }, }; setModules(); initSetupModeState(angularStateMock.scope, angularStateMock.injector); @@ -176,26 +173,26 @@ describe('setup_mode', () => { expect(state.enabled).toBe(false); expect(addDanger).toHaveBeenCalledWith({ title: 'Setup mode is not available', - text: 'You do not have the necessary permissions to do this.' + text: 'You do not have the necessary permissions to do this.', }); done(); }); }); - it('should set the newly discovered cluster uuid', async (done) => { + it('should set the newly discovered cluster uuid', async done => { const clusterUuid = '1ajy'; data = { _meta: { liveClusterUuid: clusterUuid, - hasPermissions: true + hasPermissions: true, }, elasticsearch: { byUuid: { 123: { - isPartiallyMigrated: true - } - } - } + isPartiallyMigrated: true, + }, + }, + }, }; initSetupModeState(angularStateMock.scope, angularStateMock.injector); await toggleSetupMode(true); @@ -205,20 +202,20 @@ describe('setup_mode', () => { }); }); - it('should fetch data for a given cluster', async (done) => { + it('should fetch data for a given cluster', async done => { const clusterUuid = '1ajy'; data = { _meta: { liveClusterUuid: clusterUuid, - hasPermissions: true + hasPermissions: true, }, elasticsearch: { byUuid: { 123: { - isPartiallyMigrated: true - } - } - } + isPartiallyMigrated: true, + }, + }, + }, }; initSetupModeState(angularStateMock.scope, angularStateMock.injector); diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index da23d4b77a323..b0367bc078473 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -68,21 +68,21 @@ export class BulkUploader { /* * Start the interval timer - * @param {CollectorSet} collectorSet object to use for initial the fetch/upload and fetch/uploading on interval + * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval * @return undefined */ - start(collectorSet) { + start(usageCollection) { this._log.info('Starting monitoring stats collection'); - const filterCollectorSet = _collectorSet => { + const filterCollectorSet = _usageCollection => { const successfulUploadInLastDay = this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - return _collectorSet.getFilteredCollectorSet(c => { + return _usageCollection.getFilteredCollectorSet(c => { // this is internal bulk upload, so filter out API-only collectors if (c.ignoreForInternalUploader) { return false; } // Only collect usage data at the same interval as telemetry would (default to once a day) - if (successfulUploadInLastDay && _collectorSet.isUsageCollector(c)) { + if (successfulUploadInLastDay && _usageCollection.isUsageCollector(c)) { return false; } return true; @@ -92,11 +92,11 @@ export class BulkUploader { if (this._timer) { clearInterval(this._timer); } else { - this._fetchAndUpload(filterCollectorSet(collectorSet)); // initial fetch + this._fetchAndUpload(filterCollectorSet(usageCollection)); // initial fetch } this._timer = setInterval(() => { - this._fetchAndUpload(filterCollectorSet(collectorSet)); + this._fetchAndUpload(filterCollectorSet(usageCollection)); }, this._interval); } @@ -121,12 +121,12 @@ export class BulkUploader { } /* - * @param {CollectorSet} collectorSet + * @param {usageCollection} usageCollection * @return {Promise} - resolves to undefined */ - async _fetchAndUpload(collectorSet) { - const collectorsReady = await collectorSet.areAllCollectorsReady(); - const hasUsageCollectors = collectorSet.some(collectorSet.isUsageCollector); + async _fetchAndUpload(usageCollection) { + const collectorsReady = await usageCollection.areAllCollectorsReady(); + const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); if (!collectorsReady) { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { @@ -136,8 +136,8 @@ export class BulkUploader { return; } - const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser); - const payload = this.toBulkUploadFormat(compact(data), collectorSet); + const data = await usageCollection.bulkFetch(this._callClusterWithInternalUser); + const payload = this.toBulkUploadFormat(compact(data), usageCollection); if (payload) { try { @@ -202,7 +202,7 @@ export class BulkUploader { * } * ] */ - toBulkUploadFormat(rawData, collectorSet) { + toBulkUploadFormat(rawData, usageCollection) { if (rawData.length === 0) { return; } @@ -210,7 +210,7 @@ export class BulkUploader { // convert the raw data to a nested object by taking each payload through // its formatter, organizing it per-type const typesNested = rawData.reduce((accum, { type, result }) => { - const { type: uploadType, payload: uploadData } = collectorSet.getCollectorByType(type).formatForBulkUpload(result); + const { type: uploadType, payload: uploadData } = usageCollection.getCollectorByType(type).formatForBulkUpload(result); return defaultsDeep(accum, { [uploadType]: uploadData }); }, {}); // convert the nested object into a flat array, with each payload prefixed diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js index 25efc63fafb5d..5d2ebf8dc2abc 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js @@ -19,8 +19,8 @@ const TYPES = [ /** * Fetches saved object counts by querying the .kibana index */ -export function getKibanaUsageCollector({ collectorSet, config }) { - return collectorSet.makeUsageCollector({ +export function getKibanaUsageCollector(usageCollection, config) { + return usageCollection.makeUsageCollector({ type: KIBANA_USAGE_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js index f1f47761d9f0c..2c0250fb78592 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js @@ -49,14 +49,13 @@ class OpsMonitor { /* * Initialize a collector for Kibana Ops Stats */ -export function getOpsStatsCollector({ +export function getOpsStatsCollector(usageCollection, { elasticsearchPlugin, kbnServerConfig, log, config, getOSInfo, hapiServer, - collectorSet }) { const buffer = opsBuffer({ log, config, getOSInfo }); const interval = kbnServerConfig.get('ops.interval'); @@ -85,7 +84,7 @@ export function getOpsStatsCollector({ }, 5 * 1000); // wait 5 seconds to avoid race condition with reloading logging configuration }); - return collectorSet.makeStatsCollector({ + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE_MONITORING, init: opsMonitor.start, isReady: () => { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index bb561ddda42ab..2a56deaad4f8a 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -46,8 +46,8 @@ export async function checkForEmailValue( } } -export function getSettingsCollector({ config, collectorSet }) { - return collectorSet.makeStatsCollector({ +export function getSettingsCollector(usageCollection, config) { + return usageCollection.makeStatsCollector({ type: KIBANA_SETTINGS_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js index 3c8eb5ebdf2d3..1099a23dea103 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getKibanaUsageCollector } from './get_kibana_usage_collector'; -export { getOpsStatsCollector } from './get_ops_stats_collector'; -export { getSettingsCollector } from './get_settings_collector'; +import { getKibanaUsageCollector } from './get_kibana_usage_collector'; +import { getOpsStatsCollector } from './get_ops_stats_collector'; +import { getSettingsCollector } from './get_settings_collector'; + +export function registerCollectors(usageCollection, collectorsConfigs) { + const { config } = collectorsConfigs; + + usageCollection.registerCollector(getOpsStatsCollector(usageCollection, collectorsConfigs)); + usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, config)); + usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); +} diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js index ae691f49e2b80..c202fe9589ab3 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js @@ -5,3 +5,4 @@ */ export { initBulkUploader } from './init'; +export { registerCollectors } from './collectors'; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js index bb42dad26786a..36f085c424881 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js @@ -13,6 +13,17 @@ const liveClusterUuid = 'a12'; const mockReq = (searchResult = {}) => { return { server: { + newPlatform: { + setup: { + plugins: { + usageCollection: { + getCollectorByType: () => ({ + isReady: () => false + }), + }, + }, + }, + }, config() { return { get: sinon.stub() diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index d25d8af4aaa20..540de7d1e3a7f 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -273,13 +273,15 @@ function shouldSkipBucket(product, bucket) { return false; } -async function getLiveKibanaInstance(req) { - const { collectorSet } = req.server.usage; - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); +async function getLiveKibanaInstance(usageCollection) { + if (!usageCollection) { + return null; + } + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return null; } - return collectorSet.toApiFieldNames(await kibanaStatsCollector.fetch()); + return usageCollection.toApiFieldNames(await kibanaStatsCollector.fetch()); } async function getLiveElasticsearchClusterUuid(req) { @@ -341,9 +343,11 @@ async function getLiveElasticsearchCollectionEnabled(req) { * @param {*} skipLiveData Optional and will not make any live api calls if set to true */ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => { + const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); const hasPermissions = await hasNecessaryPermissions(req); + if (!hasPermissions) { return { _meta: { @@ -351,6 +355,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU } }; } + console.log('OKOKOKOK'); const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; @@ -372,7 +377,8 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req); - const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req); + const { usageCollection } = req.server.newPlatform.setup.plugins; + const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(usageCollection); const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []); const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req); diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index 48a02109a3f6f..97930610e0593 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -9,35 +9,27 @@ import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants' import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; -import { initBulkUploader } from './kibana_monitoring'; +import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; -import { - getKibanaUsageCollector, - getOpsStatsCollector, - getSettingsCollector, -} from './kibana_monitoring/collectors'; - export class Plugin { setup(core, plugins) { const kbnServer = core._kbnServer; const config = core.config(); - const { collectorSet } = core.usage; + const usageCollection = plugins.usageCollection; + registerMonitoringCollection(); /* * Register collector objects for stats to show up in the APIs */ - collectorSet.register(getOpsStatsCollector({ + registerCollectors(usageCollection, { elasticsearchPlugin: plugins.elasticsearch, kbnServerConfig: kbnServer.config, log: core.log, config, getOSInfo: core.getOSInfo, hapiServer: core._hapi, - collectorSet: core.usage.collectorSet, - })); - collectorSet.register(getKibanaUsageCollector({ collectorSet, config })); - collectorSet.register(getSettingsCollector({ collectorSet, config })); - registerMonitoringCollection(); + }); + /* * Instantiate and start the internal background task that calls collector @@ -110,7 +102,7 @@ export class Plugin { const mainMonitoring = xpackMainInfo.feature('monitoring'); const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable() && mainMonitoring.isEnabled(); if (monitoringBulkEnabled) { - bulkUploader.start(collectorSet); + bulkUploader.start(usageCollection); } else { bulkUploader.handleNotEnabled(); } diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js index c1425de20d146..7b300939bd470 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; -describe('get_all_stats', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_all_stats', () => { const size = 123; const start = 0; const end = 1; diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index e3153670ac58f..c6bb368745830 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -9,7 +9,7 @@ import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; describe('get_cluster_uuids', () => { - const callWith = sinon.stub(); + const callCluster = sinon.stub(); const size = 123; const server = { config: sinon.stub().returns({ @@ -28,23 +28,23 @@ describe('get_cluster_uuids', () => { } } }; - const expectedUuids = response.aggregations.cluster_uuids.buckets.map(bucket => bucket.key); + const expectedUuids = response.aggregations.cluster_uuids.buckets + .map(bucket => bucket.key) + .map(expectedUuid => ({ clusterUuid: expectedUuid })); const start = new Date(); const end = new Date(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); - - expect(await getClusterUuids(server, callWith, start, end)).to.eql(expectedUuids); + callCluster.withArgs('search').returns(Promise.resolve(response)); + expect(await getClusterUuids({ server, callCluster, start, end })).to.eql(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { - callWith.returns(Promise.resolve(response)); - - expect(await fetchClusterUuids(server, callWith, start, end)).to.be(response); + callCluster.returns(Promise.resolve(response)); + expect(await fetchClusterUuids({ server, callCluster, start, end })).to.be(response); }); }); @@ -52,13 +52,11 @@ describe('get_cluster_uuids', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusterUuids = handleClusterUuidsResponse({}); - expect(clusterUuids.length).to.be(0); }); it('handles valid response', () => { const clusterUuids = handleClusterUuidsResponse(response); - expect(clusterUuids).to.eql(expectedUuids); }); diff --git a/x-pack/legacy/plugins/oss_telemetry/index.d.ts b/x-pack/legacy/plugins/oss_telemetry/index.d.ts index 012f987627369..1b592dabf2053 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.d.ts +++ b/x-pack/legacy/plugins/oss_telemetry/index.d.ts @@ -54,12 +54,6 @@ export interface HapiServer { }>; }; }; - usage: { - collectorSet: { - register: (collector: any) => void; - makeUsageCollector: (collectorOpts: any) => void; - }; - }; config: () => { get: (prop: string) => any; }; diff --git a/x-pack/legacy/plugins/oss_telemetry/index.js b/x-pack/legacy/plugins/oss_telemetry/index.js index eeee9e18f9112..f86baef020aa2 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.js +++ b/x-pack/legacy/plugins/oss_telemetry/index.js @@ -15,7 +15,8 @@ export const ossTelemetry = (kibana) => { configPrefix: 'xpack.oss_telemetry', init(server) { - registerCollectors(server); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCollectors(usageCollection, server); registerTasks(server); scheduleTasks(server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index 8b825b13178f2..0121ed4304d26 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; -export function registerCollectors(server: HapiServer) { - registerVisualizationsCollector(server); +export function registerCollectors(usageCollection: UsageCollectionSetup, server: HapiServer) { + registerVisualizationsCollector(usageCollection, server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts index 555c7ac27b49d..09843a6f87ad7 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../../'; import { getUsageCollector } from './get_usage_collector'; -export function registerVisualizationsCollector(server: HapiServer): void { - const { usage } = server; - const collector = usage.collectorSet.makeUsageCollector(getUsageCollector(server)); - usage.collectorSet.register(collector); +export function registerVisualizationsCollector( + usageCollection: UsageCollectionSetup, + server: HapiServer +): void { + const collector = usageCollection.makeUsageCollector(getUsageCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 998a1d2beeab1..1cebe78b9c7f0 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -54,12 +54,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => mockConfig, log: () => undefined, }); diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index cf8cf37d2e4ee..bf309c65556a8 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -249,7 +249,7 @@ Array [ > +
+

+ Unable to fetch report info +

+
+
+
+
+ Could not fetch the job info +
+
+
+ + } - tabIndex={1} - /> -
- + +
+ - - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
- - - - + + + + + +
-
- -

- Unable to fetch report info -

-
-
- - + Unable to fetch report info + + +
+
+ +
-
- -
- Could not fetch the job info -
-
-
+ +
+ Could not fetch the job info +
+
- -
+
+
-
+
- -
- - - - + } + tabIndex={0} + /> + +
+ + + ,
- - + + + } /> - - - } - /> - - + + - - -
- -
+
-
+
+ + } - tabIndex={1} - /> -
- + +
+ - - - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
- - - - + + + + + +
-
- -

- Job Info -

-
-
- - + Job Info + + +
+
+ +
-
- -
- -
+ +
+
- -
+
+
-
+
- -
- - - - + } + tabIndex={0} + /> + +
+ + + ,
; + export const EuiBasicTable: React.FC; } import { i18n } from '@kbn/i18n'; 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 a7e81093c136a..174c6d587e523 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -9,12 +9,11 @@ import { ServerFacade, QueueConfig } from '../../types'; // @ts-ignore import { Esqueue } from './esqueue'; import { createWorkerFactory } from './create_worker'; -import { oncePerServer } from './once_per_server'; import { LevelLogger } from './level_logger'; // @ts-ignore import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed -function createQueueFn(server: ServerFacade): Esqueue { +export function createQueueFactory(server: ServerFacade): Esqueue { const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue'); const index = server.config().get('xpack.reporting.index'); @@ -45,5 +44,3 @@ function createQueueFn(server: ServerFacade): Esqueue { return queue; } - -export const createQueueFactory = oncePerServer(createQueueFn); 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 1cfc967cb31d1..0a86f9d1d4ff5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -21,9 +21,8 @@ import { // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; import { LevelLogger } from './level_logger'; -import { oncePerServer } from './once_per_server'; -function createWorkerFn(server: ServerFacade) { +export function createWorkerFactory(server: ServerFacade) { const config = server.config(); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'queue-worker']); const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); @@ -79,5 +78,3 @@ function createWorkerFn(server: ServerFacade) { }); }; } - -export const createWorkerFactory = oncePerServer(createWorkerFn); 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 c264c0ca7e0eb..9b1d7992283a5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -7,8 +7,8 @@ import { get } from 'lodash'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; -import { oncePerServer } from './once_per_server'; import { + Job, ServerFacade, RequestFacade, Logger, @@ -24,7 +24,7 @@ interface ConfirmedJob { _primary_term: number; } -function enqueueJobFn(server: ServerFacade) { +export function enqueueJobFactory(server: ServerFacade) { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); const browserType = captureConfig.browser.type; @@ -37,9 +37,9 @@ function enqueueJobFn(server: ServerFacade) { exportTypeId: string, jobParams: object, user: string, - headers: ConditionalHeaders, + headers: ConditionalHeaders['headers'], request: RequestFacade - ) { + ): Promise { const logger = parentLogger.clone(['queue-job']); const exportType = exportTypesRegistry.getById(exportTypeId); const createJob = exportType.createJobFactory(server); @@ -65,5 +65,3 @@ function enqueueJobFn(server: ServerFacade) { }); }; } - -export const enqueueJobFactory = oncePerServer(enqueueJobFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js index 195a0d4fdbec4..6d638e50af476 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../../common/cancellation_token'; -describe('CancellationToken', function () { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('CancellationToken', function () { let cancellationToken; beforeEach(function () { cancellationToken = new CancellationToken(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 84549d0680ff3..b2e87482b73a1 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -26,6 +26,7 @@ const defaultWorkerOptions = { intervalErrorMultiplier: 10 }; + describe('Worker class', function () { // some of these tests might be a little slow, give them a little extra time this.timeout(10000); @@ -1068,7 +1069,8 @@ describe('Format Job Object', () => { }); }); -describe('Get Doc Path from ES Response', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function () { const responseMock = { _index: 'foo', diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.js b/x-pack/legacy/plugins/reporting/server/lib/get_user.js index 2c4f3bcb2dd36..04c9516cb99d4 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.js +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { oncePerServer } from './once_per_server'; - -function getUserFn(server) { +export function getUserFactory(server) { return async request => { if (!server.plugins.security) { return null; @@ -20,5 +18,3 @@ function getUserFn(server) { } }; } - -export const getUserFactory = oncePerServer(getUserFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js index e4f501e2c9518..fef9a529f7a2c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js @@ -5,11 +5,10 @@ */ import { get } from 'lodash'; -import { oncePerServer } from './once_per_server'; const defaultSize = 10; -function jobsQueryFn(server) { +export function jobsQueryFactory(server) { const index = server.config().get('xpack.reporting.index'); const { callWithInternalUser, errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); @@ -138,5 +137,3 @@ function jobsQueryFn(server) { } }; } - -export const jobsQueryFactory = oncePerServer(jobsQueryFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js index 9a74ba63b8e31..8b5d6f4591ff5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { validateConfig } from '../validate_config'; -describe('Reporting: Validate config', () => { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('Reporting: Validate config', () => { const logger = { warning: sinon.spy(), }; 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 557f7c3702038..2303fddf555e0 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 @@ -12,10 +12,10 @@ import { ResponseFacade, ReportingResponseToolkit, Logger, - JobDocPayload, JobIDForImmediate, JobDocOutputExecuted, } from '../../types'; +import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { getRouteOptionsCsv } from './lib/route_config_factories'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; @@ -48,7 +48,11 @@ export function registerGenerateCsvFromSavedObjectImmediate( const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); const createJobFn = createJobFactory(server); const executeJobFn = executeJobFactory(server); - const jobDocPayload: JobDocPayload = await createJobFn(jobParams, request.headers, request); + const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + jobParams, + request.headers, + request + ); const { content_type: jobOutputContentType, content: jobOutputContent, diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js index 10ff9f477f424..59317ac46773b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js @@ -6,11 +6,10 @@ import boom from 'boom'; import { getUserFactory } from '../../lib/get_user'; -import { oncePerServer } from '../../lib/once_per_server'; const superuserRole = 'superuser'; -function authorizedUserPreRoutingFn(server) { +export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn(server) { const getUser = getUserFactory(server); const config = server.config(); @@ -40,6 +39,4 @@ function authorizedUserPreRoutingFn(server) { return user; }; -} - -export const authorizedUserPreRoutingFactory = oncePerServer(authorizedUserPreRoutingFn); +}; 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 d3e9981a62b6e..9952cbb980778 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 @@ -13,7 +13,6 @@ import { JobDocExecuted, JobDocOutputExecuted, } from '../../../types'; -import { oncePerServer } from '../../lib/once_per_server'; import { CSV_JOB_TYPE } from '../../../common/constants'; interface ICustomHeaders { @@ -39,7 +38,7 @@ const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTyp return metaDataHeaders; }; -function getDocumentPayloadFn(server: ServerFacade) { +export function getDocumentPayloadFactory(server: ServerFacade) { const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry; function encodeContent(content: string | null, exportType: ExportTypeDefinition) { @@ -105,5 +104,3 @@ function getDocumentPayloadFn(server: ServerFacade) { return getIncomplete(status); }; } - -export const getDocumentPayloadFactory = oncePerServer(getDocumentPayloadFn); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js index 75bffcafc5c33..758c50816c381 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js @@ -5,12 +5,11 @@ */ import boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { getDocumentPayloadFactory } from './get_document_payload'; -function jobResponseHandlerFn(server) { +export function jobResponseHandlerFactory(server) { const jobsQuery = jobsQueryFactory(server); const getDocumentPayload = getDocumentPayloadFactory(server); @@ -45,5 +44,3 @@ function jobResponseHandlerFn(server) { }); }; } - -export const jobResponseHandlerFactory = oncePerServer(jobResponseHandlerFn); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js index ad91e5a654a4e..92973e3d0b422 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js @@ -5,9 +5,8 @@ */ import Boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; -function reportingFeaturePreRoutingFn(server) { +export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn(server) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'reporting'; @@ -24,6 +23,4 @@ function reportingFeaturePreRoutingFn(server) { } }; }; -} - -export const reportingFeaturePreRoutingFactory = oncePerServer(reportingFeaturePreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js deleted file mode 100644 index 32022c6fa642c..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js +++ /dev/null @@ -1,384 +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 sinon from 'sinon'; -import { getReportingUsageCollector } from './get_reporting_usage_collector'; - -function getServerMock(customization) { - class MockUsageCollector { - constructor(_server, { fetch }) { - this.fetch = fetch; - } - } - - 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 }), - }, - }, - }, - expose: () => {}, - log: () => {}, - config: () => ({ - get: key => { - if (key === 'xpack.reporting.enabled') { - return true; - } else if (key === 'xpack.reporting.index') { - return '.reporting-index'; - } - }, - }), - usage: { - collectorSet: { - makeUsageCollector: options => { - return new MockUsageCollector(this, options); - }, - }, - }, - }; - return Object.assign(defaultServerMock, customization); -} - -const getResponseMock = (customization = {}) => customization; - -describe('license checks', () => { - describe('with a basic license', () => { - let usageStats; - beforeAll(async () => { - const serverWithBasicLicenseMock = getServerMock(); - serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('basic'); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - - test('sets pdf availability to false', async () => { - expect(usageStats.printable_pdf.available).toBe(false); - }); - }); - - describe('with no license', () => { - let usageStats; - beforeAll(async () => { - const serverWithNoLicenseMock = getServerMock(); - serverWithNoLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('none'); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock); - usageStats = await getReportingUsage(callClusterMock); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to false', async () => { - expect(usageStats.csv.available).toBe(false); - }); - - test('sets pdf availability to false', async () => { - expect(usageStats.printable_pdf.available).toBe(false); - }); - }); - - describe('with platinum license', () => { - let usageStats; - beforeAll(async () => { - const serverWithPlatinumLicenseMock = getServerMock(); - serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('platinum'); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector( - serverWithPlatinumLicenseMock - ); - usageStats = await getReportingUsage(callClusterMock); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - - test('sets pdf availability to true', async () => { - expect(usageStats.printable_pdf.available).toBe(true); - }); - }); - - describe('with no usage data', () => { - let usageStats; - beforeAll(async () => { - const serverWithBasicLicenseMock = getServerMock(); - serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('basic'); - const callClusterMock = jest.fn(() => Promise.resolve({})); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - }); -}); - -describe('data modeling', () => { - let getReportingUsage; - beforeAll(async () => { - const serverWithPlatinumLicenseMock = getServerMock(); - serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock)); - }); - - test('with normal looking usage data', async () => { - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 54, - layoutTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'preserve_layout', doc_count: 13 }, - { key: 'print', doc_count: 10 }, - ], - }, - }, - objectTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 23 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'pending', doc_count: 33 }, - { key: 'completed', doc_count: 20 }, - { key: 'processing', doc_count: 1 }, - ], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 27 }, - { key: 'printable_pdf', doc_count: 23 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - lastDay: { - doc_count: 11, - layoutTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'print', doc_count: 2 }], - }, - }, - objectTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 2 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 11 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 5 }, - { key: 'PNG', doc_count: 4 }, - { key: 'printable_pdf', doc_count: 2 }, - ], - }, - }, - last7Days: { - doc_count: 27, - layoutTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'print', doc_count: 10 }, - { key: 'preserve_layout', doc_count: 3 }, - ], - }, - }, - objectTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 13 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 27 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'printable_pdf', doc_count: 13 }, - { key: 'csv', doc_count: 10 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - }, - }, - }, - }) - ) - ); - const usageStats = await getReportingUsage(callClusterMock); - expect(usageStats).toMatchInlineSnapshot(` -Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 54, - "available": true, - "browser_type": undefined, - "csv": Object { - "available": true, - "total": 27, - }, - "enabled": true, - "last7Days": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 27, - "csv": Object { - "available": true, - "total": 10, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 13, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 3, - "print": 10, - }, - "total": 13, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 27, - }, - }, - "lastDay": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 11, - "csv": Object { - "available": true, - "total": 5, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 2, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 0, - "print": 2, - }, - "total": 2, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 11, - }, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 23, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 13, - "print": 10, - }, - "total": 23, - }, - "status": Object { - "completed": 20, - "failed": 0, - "pending": 33, - "processing": 1, - }, -} -`); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts deleted file mode 100644 index 5c52193769057..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// @ts-ignore untyped module -import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; -import { ServerFacade, ESCallCluster } from '../../types'; -import { KIBANA_REPORTING_TYPE } from '../../common/constants'; -import { getReportingUsage } from './get_reporting_usage'; -import { RangeStats } from './types'; - -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function getReportingUsageCollector(server: ServerFacade, isReady: () => boolean) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ - type: KIBANA_REPORTING_TYPE, - isReady, - fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), - - /* - * Format the response data into a model for internal upload - * 1. Make this data part of the "kibana_stats" type - * 2. Organize the payload in the usage.xpack.reporting namespace of the data payload - */ - formatForBulkUpload: (result: RangeStats) => { - return { - type: KIBANA_STATS_TYPE_MONITORING, - payload: { - usage: { - xpack: { - reporting: result, - }, - }, - }, - }; - }, - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/usage/index.ts b/x-pack/legacy/plugins/reporting/server/usage/index.ts index 91e2a9284550b..141ecb9c77656 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/index.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getReportingUsageCollector } from './get_reporting_usage_collector'; +export { registerReportingUsageCollector } from './reporting_usage_collector'; 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 new file mode 100644 index 0000000000000..f23f679865146 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -0,0 +1,390 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 sinon from 'sinon'; +import { getReportingUsageCollector } from './reporting_usage_collector'; + +function getMockUsageCollection() { + class MockUsageCollector { + constructor(_server, { fetch }) { + this.fetch = fetch; + } + } + return { + makeUsageCollector: options => { + return new MockUsageCollector(this, options); + }, + }; +} + +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 }), + }, + }, + }, + expose: () => {}, + log: () => {}, + config: () => ({ + get: key => { + if (key === 'xpack.reporting.enabled') { + return true; + } else if (key === 'xpack.reporting.index') { + return '.reporting-index'; + } + }, + }), + }; + return Object.assign(defaultServerMock, customization); +} + +const getResponseMock = (customization = {}) => customization; + +describe('license checks', () => { + describe('with a basic license', () => { + let usageStats; + beforeAll(async () => { + const serverWithBasicLicenseMock = getServerMock(); + serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon + .stub() + .returns('basic'); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); + usageStats = await getReportingUsage(callClusterMock); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + + test('sets pdf availability to false', async () => { + expect(usageStats.printable_pdf.available).toBe(false); + }); + }); + + describe('with no license', () => { + let usageStats; + beforeAll(async () => { + const serverWithNoLicenseMock = getServerMock(); + serverWithNoLicenseMock.plugins.xpack_main.info.license.getType = sinon + .stub() + .returns('none'); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock); + usageStats = await getReportingUsage(callClusterMock); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to false', async () => { + expect(usageStats.csv.available).toBe(false); + }); + + test('sets pdf availability to false', async () => { + expect(usageStats.printable_pdf.available).toBe(false); + }); + }); + + describe('with platinum license', () => { + let usageStats; + beforeAll(async () => { + const serverWithPlatinumLicenseMock = getServerMock(); + serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon + .stub() + .returns('platinum'); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithPlatinumLicenseMock + ); + usageStats = await getReportingUsage(callClusterMock); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + + test('sets pdf availability to true', async () => { + expect(usageStats.printable_pdf.available).toBe(true); + }); + }); + + describe('with no usage data', () => { + let usageStats; + beforeAll(async () => { + const serverWithBasicLicenseMock = getServerMock(); + serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon + .stub() + .returns('basic'); + const callClusterMock = jest.fn(() => Promise.resolve({})); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); + usageStats = await getReportingUsage(callClusterMock); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + }); +}); + +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(usageCollection, serverWithPlatinumLicenseMock)); + }); + + test('with normal looking usage data', async () => { + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 54, + layoutTypes: { + doc_count: 23, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'preserve_layout', doc_count: 13 }, + { key: 'print', doc_count: 10 }, + ], + }, + }, + objectTypes: { + doc_count: 23, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 23 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'pending', doc_count: 33 }, + { key: 'completed', doc_count: 20 }, + { key: 'processing', doc_count: 1 }, + ], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'csv', doc_count: 27 }, + { key: 'printable_pdf', doc_count: 23 }, + { key: 'PNG', doc_count: 4 }, + ], + }, + }, + lastDay: { + doc_count: 11, + layoutTypes: { + doc_count: 2, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'print', doc_count: 2 }], + }, + }, + objectTypes: { + doc_count: 2, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 2 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'pending', doc_count: 11 }], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'csv', doc_count: 5 }, + { key: 'PNG', doc_count: 4 }, + { key: 'printable_pdf', doc_count: 2 }, + ], + }, + }, + last7Days: { + doc_count: 27, + layoutTypes: { + doc_count: 13, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'print', doc_count: 10 }, + { key: 'preserve_layout', doc_count: 3 }, + ], + }, + }, + objectTypes: { + doc_count: 13, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 13 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'pending', doc_count: 27 }], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'printable_pdf', doc_count: 13 }, + { key: 'csv', doc_count: 10 }, + { key: 'PNG', doc_count: 4 }, + ], + }, + }, + }, + }, + }, + }) + ) + ); + const usageStats = await getReportingUsage(callClusterMock); + expect(usageStats).toMatchInlineSnapshot(` +Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 54, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 27, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 27, + "csv": Object { + "available": true, + "total": 10, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 13, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 3, + "print": 10, + }, + "total": 13, + }, + "status": Object { + "completed": 0, + "failed": 0, + "pending": 27, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 11, + "csv": Object { + "available": true, + "total": 5, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 2, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 2, + }, + "total": 2, + }, + "status": Object { + "completed": 0, + "failed": 0, + "pending": 11, + }, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 23, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 13, + "print": 10, + }, + "total": 23, + }, + "status": Object { + "completed": 20, + "failed": 0, + "pending": 33, + "processing": 1, + }, +} +`); + }); +}); 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 new file mode 100644 index 0000000000000..0a7ef0a194434 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +// @ts-ignore untyped module +import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; +import { ServerFacade, ESCallCluster } from '../../types'; +import { KIBANA_REPORTING_TYPE } from '../../common/constants'; +import { getReportingUsage } from './get_reporting_usage'; +import { RangeStats } from './types'; + +/* + * @param {Object} server + * @return {Object} kibana usage stats type collection object + */ +export function getReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + return usageCollection.makeUsageCollector({ + type: KIBANA_REPORTING_TYPE, + isReady, + fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage.xpack.reporting namespace of the data payload + */ + formatForBulkUpload: (result: RangeStats) => { + return { + type: KIBANA_STATS_TYPE_MONITORING, + payload: { + usage: { + xpack: { + reporting: result, + }, + }, + }, + }; + }, + }); +} + +export function registerReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + const collector = getReportingUsageCollector(usageCollection, server, isReady); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 6d2808c5b560d..e8fb015426f51 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -16,7 +16,12 @@ import { CancellationToken } from './common/cancellation_token'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -type Job = EventEmitter & { id: string }; +export type Job = EventEmitter & { + id: string; + toJSON: () => { + id: string; + }; +}; export interface ReportingPlugin { queue: { @@ -193,9 +198,10 @@ export interface JobParamPostPayload { export interface JobDocPayload { headers?: Record; - jobParams: object; + jobParams: any; title: string; type: string | null; + objects?: null | object[]; } export interface JobSource { @@ -245,11 +251,29 @@ export interface ESQueueWorker { on: (event: string, handler: any) => void; } +type JobParamsUrl = object; + +interface JobParamsSavedObject { + savedObjectType: string; + savedObjectId: string; + isImmediate: boolean; +} + export type ESQueueCreateJobFn = ( - jobParams: object, - headers: ConditionalHeaders, + jobParams: JobParamsSavedObject | JobParamsUrl, + headers: Record, request: RequestFacade -) => Promise; +) => Promise; + +export type ImmediateCreateJobFn = ( + jobParams: any, + headers: Record, + req: RequestFacade +) => Promise<{ + type: string | null; + title: string; + jobParams: any; +}>; export type ESQueueWorkerExecuteFn = ( jobId: string, @@ -258,9 +282,10 @@ export type ESQueueWorkerExecuteFn = ( ) => void; export type JobIDForImmediate = null; + export type ImmediateExecuteFn = ( jobId: JobIDForImmediate, - jobDocPayload: JobDocPayload, + job: JobDocPayload, request: RequestFacade ) => Promise; @@ -279,9 +304,8 @@ export interface ESQueueInstance { ) => ESQueueWorker; } -export type CreateJobFactory = (server: ServerFacade) => ESQueueCreateJobFn; -export type ExecuteJobFactory = (server: ServerFacade) => ESQueueWorkerExecuteFn; -export type ExecuteImmediateJobFactory = (server: ServerFacade) => ImmediateExecuteFn; +export type CreateJobFactory = (server: ServerFacade) => ESQueueCreateJobFn | ImmediateCreateJobFn; +export type ExecuteJobFactory = (server: ServerFacade) => ESQueueWorkerExecuteFn | ImmediateExecuteFn; // prettier-ignore export interface ExportTypeDefinition { id: string; @@ -290,7 +314,7 @@ export interface ExportTypeDefinition { jobContentEncoding?: string; jobContentExtension: string; createJobFactory: CreateJobFactory; - executeJobFactory: ExecuteJobFactory | ExecuteImmediateJobFactory; + executeJobFactory: ExecuteJobFactory; validLicenses: string[]; } @@ -302,3 +326,10 @@ export { CancellationToken } from './common/cancellation_token'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger } from './server/lib/level_logger'; + +export interface AbsoluteURLFactoryOptions { + defaultBasePath: string; + protocol: string; + hostname: string; + port: string | number; +} diff --git a/x-pack/legacy/plugins/rollup/index.js b/x-pack/legacy/plugins/rollup/index.js index 3b6c033a2d85a..e0c00a7db62f0 100644 --- a/x-pack/legacy/plugins/rollup/index.js +++ b/x-pack/legacy/plugins/rollup/index.js @@ -57,12 +57,13 @@ export function rollup(kibana) { ], }, init: function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; registerLicenseChecker(server); registerIndicesRoute(server); registerFieldsForWildcardRoute(server); registerSearchRoute(server); registerJobsRoute(server); - registerRollupUsageCollector(server); + registerRollupUsageCollector(usageCollection, server); if ( server.plugins.index_management && server.plugins.index_management.addIndexManagementDataEnricher diff --git a/x-pack/legacy/plugins/rollup/public/search/register.js b/x-pack/legacy/plugins/rollup/public/search/register.js index 917ee872254f5..f7f1c681b63ca 100644 --- a/x-pack/legacy/plugins/rollup/public/search/register.js +++ b/x-pack/legacy/plugins/rollup/public/search/register.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { addSearchStrategy } from 'ui/courier'; +import { addSearchStrategy } from '../../../../../../src/legacy/ui/public/courier'; import { rollupSearchStrategy } from './rollup_search_strategy'; export function initSearch() { diff --git a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js index ab24a37a2ecec..28f08ba1ab952 100644 --- a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js @@ -5,7 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { SearchError, getSearchErrorType } from 'ui/courier'; +import { SearchError, getSearchErrorType } from '../../../../../../src/legacy/ui/public/courier'; function serializeFetchParams(searchRequests) { return JSON.stringify(searchRequests.map(searchRequestWithFetchParams => { diff --git a/x-pack/legacy/plugins/rollup/server/usage/collector.js b/x-pack/legacy/plugins/rollup/server/usage/collector.js index 977253dfa53fb..99fffa774baaf 100644 --- a/x-pack/legacy/plugins/rollup/server/usage/collector.js +++ b/x-pack/legacy/plugins/rollup/server/usage/collector.js @@ -163,10 +163,10 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa }; } -export function registerRollupUsageCollector(server) { +export function registerRollupUsageCollector(usageCollection, server) { const kibanaIndex = server.config().get('kibana.index'); - const collector = server.usage.collectorSet.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: ROLLUP_USAGE_TYPE, isReady: () => true, fetch: async callCluster => { @@ -198,5 +198,5 @@ export function registerRollupUsageCollector(server) { }, }); - server.usage.collectorSet.register(collector); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx index bf27620dcac18..d709a8feb48bd 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx @@ -15,13 +15,12 @@ export const EmptyTreePlaceHolder = () => { {/* TODO: translations */}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeTitle', { - defaultMessage: 'Nothing to see here yet.', + defaultMessage: 'No queries to profile', })}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeDescription', { - defaultMessage: - 'Enter a query and press the "Profile" button or provide profile data in the editor.', + defaultMessage: 'Enter a query, click Profile, and see the results here.', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx index fb09c6cddf70a..a7db54b670a84 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx @@ -13,7 +13,7 @@ export const ProfileLoadingPlaceholder = () => {

{i18n.translate('xpack.searchProfiler.profilingLoaderText', { - defaultMessage: 'Profiling...', + defaultMessage: 'Loading query profiles...', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx index 7f5d223949e61..63ae5c7583625 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx @@ -93,7 +93,7 @@ export const Main = () => { return ( <> - + {renderLicenseWarning()} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts index dac9dab9bd092..615511786afd1 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts @@ -12,7 +12,6 @@ import { OnHighlightChangeArgs } from '../components/profile_tree'; import { ShardSerialized, Targets } from '../types'; export type Action = - | { type: 'setPristine'; value: boolean } | { type: 'setProfiling'; value: boolean } | { type: 'setHighlightDetails'; value: OnHighlightChangeArgs | null } | { type: 'setActiveTab'; value: Targets | null } @@ -20,12 +19,8 @@ export type Action = export const reducer: Reducer = (state, action) => produce(state, draft => { - if (action.type === 'setPristine') { - draft.pristine = action.value; - return; - } - if (action.type === 'setProfiling') { + draft.pristine = false; draft.profiling = action.value; if (draft.profiling) { draft.currentResponse = null; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts index 7b5a1ce93583d..7008854a16285 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts @@ -18,7 +18,7 @@ export interface State { export const initialState: State = { profiling: false, - pristine: false, + pristine: true, highlightDetails: null, activeTab: null, currentResponse: null, diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss index a72d079354f89..d36a587b9257f 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -10,12 +10,6 @@ @import 'containers/main'; @import 'containers/profile_query_editor'; -#searchProfilerAppRoot { - height: 100%; - display: flex; - flex: 1 1 auto; -} - .prfDevTool__licenseWarning { &__container { max-width: 1000px; @@ -55,19 +49,10 @@ } } -.prfDevTool { - height: calc(100vh - #{$euiHeaderChildSize}); +.appRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); overflow: hidden; - - .devApp__container { - height: 100%; - overflow: hidden; - flex-shrink: 1; - } - - &__container { - overflow: hidden; - } + flex-shrink: 1; } .prfDevTool__detail { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss index cc4d334f58fd3..c7dc4a305acb2 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss @@ -5,10 +5,6 @@ $badgeSize: $euiSize * 5.5; .prfDevTool__profileTree { - &__container { - height: 100%; - } - &__shardDetails--dim small { color: $euiColorDarkShade; } diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts index 7a5981fcb0a69..f2acc97e55a70 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts @@ -27,15 +27,15 @@ export class SearchProfilerUIPlugin implements Plugin { notifications: ToastsStart; formatAngularHttpError: any; }; - devTools: DevToolsSetup; + dev_tools: DevToolsSetup; } ) { const { http } = core; const { __LEGACY: { I18nContext, licenseEnabled, notifications, formatAngularHttpError }, - devTools, + dev_tools, } = plugins; - devTools.register({ + dev_tools.register({ id: 'searchprofiler', title: i18n.translate('xpack.searchProfiler.pageDisplayName', { defaultMessage: 'Search Profiler', diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index c098e3e67a6d9..60374d562f96c 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -29,8 +29,12 @@ export const security = (kibana) => new kibana.Plugin({ enabled: Joi.boolean().default(true), cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + session: Joi.object({ + idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -43,9 +47,10 @@ export const security = (kibana) => new kibana.Plugin({ }).default(); }, - deprecations: function ({ unused }) { + deprecations: function ({ rename, unused }) { return [ unused('authorization.legacyFallback.enabled'), + rename('sessionTimeout', 'session.idleTimeout'), ]; }, @@ -88,7 +93,11 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: securityPlugin.__legacyCompat.config.secureCookies, - sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, + session: { + tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, + idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, + lifespan: securityPlugin.__legacyCompat.config.session.lifespan, + }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, @@ -147,7 +156,9 @@ export const security = (kibana) => new kibana.Plugin({ server.injectUiAppVars('login', () => { const { showLogin, allowLogin, layout = 'form' } = securityPlugin.__legacyCompat.license.getFeatures(); + const { loginAssistanceMessage } = securityPlugin.__legacyCompat.config; return { + loginAssistanceMessage, loginState: { showLogin, allowLogin, diff --git a/x-pack/legacy/plugins/security/public/components/authentication_state_page/authentication_state_page.tsx b/x-pack/legacy/plugins/security/public/components/authentication_state_page/authentication_state_page.tsx index 2e02275b39611..aa30661129978 100644 --- a/x-pack/legacy/plugins/security/public/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/legacy/plugins/security/public/components/authentication_state_page/authentication_state_page.tsx @@ -11,7 +11,7 @@ interface Props { title: React.ReactNode; } -export const AuthenticationStatePage: React.SFC = props => ( +export const AuthenticationStatePage: React.FC = props => (
diff --git a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js index 81b14ee7d8bf4..d9fb450779411 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js @@ -7,28 +7,20 @@ import _ from 'lodash'; import { uiModules } from 'ui/modules'; import { isSystemApiRequest } from 'ui/system_api'; -import { Path } from 'plugins/xpack_main/services/path'; import { npSetup } from 'ui/new_platform'; -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - * @type {number} - */ - const module = uiModules.get('security', []); module.config(($httpProvider) => { $httpProvider.interceptors.push(( $q, ) => { - const isUnauthenticated = Path.isUnauthenticated(); + const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname); function interceptorFactory(responseHandler) { return function interceptor(response) { - if (!isUnauthenticated && !isSystemApiRequest(response.config)) { - npSetup.plugins.security.sessionTimeout.extend(); + if (!isAnonymous && !isSystemApiRequest(response.config)) { + npSetup.plugins.security.sessionTimeout.extend(response.config.url); } return responseHandler(response); }; diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx index 94a42166fbb9e..2ed057ad73a12 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx @@ -13,7 +13,7 @@ interface Props { user: AuthenticatedUser; } -export const AccountManagementPage: React.SFC = props => ( +export const AccountManagementPage: React.FC = props => ( diff --git a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx index 369b531e8ddf8..dbeb68875c1a9 100644 --- a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx +++ b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx @@ -31,7 +31,7 @@ chrome } > - + , diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 3b3024024a9cf..a08c454e569e6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -2,6 +2,20 @@ exports[`BasicLoginForm renders as expected 1`] = ` + + +
{ loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ) ).toMatchSnapshot(); @@ -68,6 +69,7 @@ describe('BasicLoginForm', () => { next={''} infoMessage={'Hey this is an info message'} intl={null as any} + loginAssistanceMessage="" /> ); @@ -86,6 +88,7 @@ describe('BasicLoginForm', () => { loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index 9dbb556f5f5f4..acdc29842d4c6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -7,6 +7,8 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { EuiText } from '@elastic/eui'; import { LoginState } from '../../../../../common/login_state'; interface Props { @@ -16,6 +18,7 @@ interface Props { loginState: LoginState; next: string; intl: InjectedIntl; + loginAssistanceMessage: string; } interface State { @@ -38,6 +41,7 @@ class BasicLoginFormUI extends Component { public render() { return ( + {this.renderLoginAssistanceMessage()} {this.renderMessage()} @@ -102,6 +106,16 @@ class BasicLoginFormUI extends Component { ); } + private renderLoginAssistanceMessage = () => { + return ( + + + {this.props.loginAssistanceMessage} + + + ); + }; + private renderMessage = () => { if (this.state.message) { return ( @@ -132,6 +146,7 @@ class BasicLoginFormUI extends Component { ); } + return null; }; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/disabled_login_form/disabled_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/disabled_login_form/disabled_login_form.tsx index 012c16c57716e..b539029d834ec 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/disabled_login_form/disabled_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/disabled_login_form/disabled_login_form.tsx @@ -12,7 +12,7 @@ interface Props { message: ReactNode; } -export const DisabledLoginForm: React.SFC = props => ( +export const DisabledLoginForm: React.FC = props => (

{props.title}

diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap index fc33c6e0a82cc..17ba81988414a 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap @@ -160,6 +160,88 @@ exports[`LoginPage disabled form states renders as expected when an unknown logi
`; +exports[`LoginPage disabled form states renders as expected when loginAssistanceMessage is set 1`] = ` +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + +
+
+`; + exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
{ loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: true, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -61,6 +62,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -76,6 +78,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -91,6 +94,21 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', + }; + + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when loginAssistanceMessage is set', () => { + const props = { + http: createMockHttp(), + window: {}, + next: '', + loginState: createLoginState(), + isSecureConnection: false, + requiresSecureConnection: false, + loginAssistanceMessage: 'This is an *important* message', }; expect(shallow()).toMatchSnapshot(); @@ -106,6 +124,7 @@ describe('LoginPage', () => { loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index 82dd0e679a5ee..e7e56947ca58f 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -31,6 +31,7 @@ interface Props { loginState: LoginState; isSecureConnection: boolean; requiresSecureConnection: boolean; + loginAssistanceMessage: string; } export class LoginPage extends Component { diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index 8b452e4c4fdf5..d9daf2d1f4d0d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -39,7 +39,8 @@ interface AnyObject { $http: AnyObject, $window: AnyObject, secureCookies: boolean, - loginState: LoginState + loginState: LoginState, + loginAssistanceMessage: string ) => { const basePath = chrome.getBasePath(); const next = parseNext($window.location.href, basePath); @@ -59,6 +60,7 @@ interface AnyObject { loginState={loginState} isSecureConnection={isSecure} requiresSecureConnection={secureCookies} + loginAssistanceMessage={loginAssistanceMessage} next={next} /> , diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap new file mode 100644 index 0000000000000..350e28123c29b --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -0,0 +1,250 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApiKeysGridPage renders a callout when API keys are not enabled 1`] = ` + + + } + > +
+
+ + + + API keys not enabled in Elasticsearch + + +
+ +
+ + + , + } + } + > + Contact your system administrator and refer to the + + + + docs + + + + to enable API keys. + +
+
+
+
+
+`; + +exports[`ApiKeysGridPage renders permission denied if user does not have required permissions 1`] = ` + + +
+ + +
+ + +

+ } + iconType="securityApp" + title={ +

+ +

+ } + > +
+ + + + + + + + + +
+ + + + +

+ + You need permission to manage API keys + +

+
+ +
+ + +
+

+ + Contact your system administrator. + +

+
+
+ + +
+ +
+ + +
+ + +`; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.test.tsx new file mode 100644 index 0000000000000..19ac3881f78d9 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.test.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. + */ + +let mockSimulate403 = false; +let mockSimulate500 = false; +let mockAreApiKeysEnabled = true; +let mockIsAdmin = true; + +const mock403 = () => ({ body: { statusCode: 403 } }); +const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); + +jest.mock('../../../../lib/api_keys_api', () => { + return { + ApiKeysApi: { + async checkPrivileges() { + if (mockSimulate403) { + throw mock403(); + } + + return { + isAdmin: mockIsAdmin, + areApiKeysEnabled: mockAreApiKeysEnabled, + }; + }, + async getApiKeys() { + if (mockSimulate500) { + throw mock500(); + } + + return { + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }; + }, + }, + }; +}); + +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ApiKeysGridPage } from './api_keys_grid_page'; +import React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + +import { NotEnabled } from './not_enabled'; +import { PermissionDenied } from './permission_denied'; + +const waitForRender = async ( + wrapper: ReactWrapper, + condition: (wrapper: ReactWrapper) => boolean +) => { + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + await Promise.resolve(); + wrapper.update(); + if (condition(wrapper)) { + resolve(); + } + }, 10); + + setTimeout(() => { + clearInterval(interval); + reject(new Error('waitForRender timeout after 2000ms')); + }, 2000); + }); +}; + +describe('ApiKeysGridPage', () => { + beforeEach(() => { + mockSimulate403 = false; + mockSimulate500 = false; + mockAreApiKeysEnabled = true; + mockIsAdmin = true; + }); + + it('renders a loading state when fetching API keys', async () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); + }); + + it('renders a callout when API keys are not enabled', async () => { + mockAreApiKeysEnabled = false; + const wrapper = mountWithIntl(); + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(NotEnabled).length > 0; + }); + + expect(wrapper.find(NotEnabled)).toMatchSnapshot(); + }); + + it('renders permission denied if user does not have required permissions', async () => { + mockSimulate403 = true; + const wrapper = mountWithIntl(); + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(PermissionDenied).length > 0; + }); + + expect(wrapper.find(PermissionDenied)).toMatchSnapshot(); + }); + + it('renders error callout if error fetching API keys', async () => { + mockSimulate500 = true; + const wrapper = mountWithIntl(); + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(EuiCallOut).length > 0; + }); + + expect(wrapper.find('EuiCallOut[data-test-subj="apiKeysError"]')).toHaveLength(1); + }); + + describe('Admin view', () => { + const wrapper = mountWithIntl(); + + it('renders a callout indicating the user is an administrator', async () => { + const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(calloutEl).length > 0; + }); + + expect(wrapper.find(calloutEl).text()).toEqual('You are an API Key administrator.'); + }); + + it('renders the correct description text', async () => { + const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(descriptionEl).length > 0; + }); + + expect(wrapper.find(descriptionEl).text()).toEqual( + 'View and invalidate API keys. An API key sends requests on behalf of a user.' + ); + }); + }); + + describe('Non-admin view', () => { + mockIsAdmin = false; + const wrapper = mountWithIntl(); + + it('does NOT render a callout indicating the user is an administrator', async () => { + const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; + const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(descriptionEl).length > 0; + }); + + expect(wrapper.find(calloutEl).length).toEqual(0); + }); + + it('renders the correct description text', async () => { + const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(descriptionEl).length > 0; + }); + + expect(wrapper.find(descriptionEl).text()).toEqual( + 'View and invalidate your API keys. An API key sends requests on your behalf.' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx index 6bebf17c943a4..37838cfdb950d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx @@ -86,7 +86,7 @@ export class ApiKeysGridPage extends Component { if (isLoadingApp) { return ( - + { } color="danger" iconType="alert" + data-test-subj="apiKeysError" > {statusCode}: {errorTitle} - {message} @@ -136,7 +137,7 @@ export class ApiKeysGridPage extends Component { } const description = ( - +

{isAdmin ? ( { color="success" iconType="user" size="s" + data-test-subj="apiKeyAdminDescriptionCallOut" /> diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index a354155fcfb0a..6af7672f6fef8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; -import React, { ReactNode, SFC } from 'react'; +import React, { ReactNode, FC } from 'react'; import { PRIVILEGE_SOURCE, PrivilegeExplanation, @@ -21,7 +21,7 @@ interface Props extends PropsOf { tooltipContent?: ReactNode; } -export const PrivilegeDisplay: SFC = (props: Props) => { +export const PrivilegeDisplay: FC = (props: Props) => { const { explanation } = props; if (!explanation) { @@ -39,7 +39,7 @@ export const PrivilegeDisplay: SFC = (props: Props) => { return ; }; -const SimplePrivilegeDisplay: SFC = (props: Props) => { +const SimplePrivilegeDisplay: FC = (props: Props) => { const { privilege, iconType, iconTooltipContent, explanation, tooltipContent, ...rest } = props; const text = ( @@ -55,7 +55,7 @@ const SimplePrivilegeDisplay: SFC = (props: Props) => { return text; }; -export const SupersededPrivilegeDisplay: SFC = (props: Props) => { +export const SupersededPrivilegeDisplay: FC = (props: Props) => { const { supersededPrivilege, actualPrivilegeSource } = props.explanation || ({} as PrivilegeExplanation); @@ -77,7 +77,7 @@ export const SupersededPrivilegeDisplay: SFC = (props: Props) => { ); }; -export const EffectivePrivilegeDisplay: SFC = (props: Props) => { +export const EffectivePrivilegeDisplay: FC = (props: Props) => { const { explanation, ...rest } = props; const source = getReadablePrivilegeSource(explanation!.actualPrivilegeSource); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx index 282ce4eea160c..91f5f048adc6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx @@ -374,7 +374,7 @@ class EditUserPageUI extends Component { -

+

{isNewUser ? ( { values={{ userName: user.username }} /> )} -

+ {reserved && ( diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts index 82f91483dc60d..55b6f735cfced 100644 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts +++ b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts @@ -37,6 +37,9 @@ export function serverFixture() { getUser: stub(), authenticate: stub(), deauthenticate: stub(), + authorization: { + application: stub(), + }, }, xpack_main: { diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js new file mode 100644 index 0000000000000..400e5b705aeb2 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { initGetApiKeysApi } from './get'; +import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; + +const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); + +describe('GET API keys', () => { + const getApiKeysTest = ( + description, + { + preCheckLicenseImpl = () => null, + callWithRequestImpl, + asserts, + isAdmin = true, + } + ) => { + test(description, async () => { + const mockServer = createMockServer(); + const pre = jest.fn().mockImplementation(preCheckLicenseImpl); + const mockCallWithRequest = jest.fn(); + + if (callWithRequestImpl) { + mockCallWithRequest.mockImplementation(callWithRequestImpl); + } + + initGetApiKeysApi(mockServer, mockCallWithRequest, pre); + + const headers = { + authorization: 'foo', + }; + + const request = { + method: 'GET', + url: `${INTERNAL_API_BASE_PATH}/api_key?isAdmin=${isAdmin}`, + headers, + }; + + const { result, statusCode } = await mockServer.inject(request); + + expect(pre).toHaveBeenCalled(); + + if (callWithRequestImpl) { + expect(mockCallWithRequest).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }), + 'shield.getAPIKeys', + { + owner: !isAdmin, + }, + ); + } else { + expect(mockCallWithRequest).not.toHaveBeenCalled(); + } + + expect(statusCode).toBe(asserts.statusCode); + expect(result).toEqual(asserts.result); + }); + }; + + describe('failure', () => { + getApiKeysTest('returns result of routePreCheckLicense', { + preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), + asserts: { + statusCode: 403, + result: { + error: 'Forbidden', + statusCode: 403, + message: 'test forbidden message', + }, + }, + }); + + getApiKeysTest('returns error from callWithRequest', { + callWithRequestImpl: async () => { + throw Boom.notAcceptable('test not acceptable message'); + }, + asserts: { + statusCode: 406, + result: { + error: 'Not Acceptable', + statusCode: 406, + message: 'test not acceptable message', + }, + }, + }); + }); + + describe('success', () => { + getApiKeysTest('returns API keys', { + callWithRequestImpl: async () => ({ + api_keys: + [{ + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }] + }), + asserts: { + statusCode: 200, + result: { + apiKeys: + [{ + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }] + }, + }, + }); + getApiKeysTest('returns only valid API keys', { + callWithRequestImpl: async () => ({ + api_keys: + [{ + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key1', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: true, + username: 'elastic', + realm: 'reserved' + }, { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }], + }), + asserts: { + statusCode: 200, + result: { + apiKeys: + [{ + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }] + }, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js new file mode 100644 index 0000000000000..3ed7ca94eb782 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { initInvalidateApiKeysApi } from './invalidate'; +import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; + +const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); + +describe('POST invalidate', () => { + const postInvalidateTest = ( + description, + { + preCheckLicenseImpl = () => null, + callWithRequestImpls = [], + asserts, + payload, + } + ) => { + test(description, async () => { + const mockServer = createMockServer(); + const pre = jest.fn().mockImplementation(preCheckLicenseImpl); + const mockCallWithRequest = jest.fn(); + + for (const impl of callWithRequestImpls) { + mockCallWithRequest.mockImplementationOnce(impl); + } + + initInvalidateApiKeysApi(mockServer, mockCallWithRequest, pre); + + const headers = { + authorization: 'foo', + }; + + const request = { + method: 'POST', + url: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`, + headers, + payload, + }; + + const { result, statusCode } = await mockServer.inject(request); + + expect(pre).toHaveBeenCalled(); + + if (asserts.callWithRequests) { + for (const args of asserts.callWithRequests) { + expect(mockCallWithRequest).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }), + ...args + ); + } + } else { + expect(mockCallWithRequest).not.toHaveBeenCalled(); + } + + expect(statusCode).toBe(asserts.statusCode); + expect(result).toEqual(asserts.result); + }); + }; + + describe('failure', () => { + postInvalidateTest('returns result of routePreCheckLicense', { + preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: true + }, + asserts: { + statusCode: 403, + result: { + error: 'Forbidden', + statusCode: 403, + message: 'test forbidden message', + }, + }, + }); + + postInvalidateTest('returns errors array from callWithRequest', { + callWithRequestImpls: [async () => { + throw Boom.notAcceptable('test not acceptable message'); + }], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], + isAdmin: true + }, + asserts: { + callWithRequests: [ + ['shield.invalidateAPIKey', { + body: { + id: 'si8If24B1bKsmSLTAhJV', + }, + }], + ], + statusCode: 200, + result: { + itemsInvalidated: [], + errors: [{ + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + error: Boom.notAcceptable('test not acceptable message'), + }] + }, + }, + }); + }); + + describe('success', () => { + postInvalidateTest('invalidates API keys', { + callWithRequestImpls: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], + isAdmin: true + }, + asserts: { + callWithRequests: [ + ['shield.invalidateAPIKey', { + body: { + id: 'si8If24B1bKsmSLTAhJV', + }, + }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], + errors: [], + }, + }, + }); + + postInvalidateTest('adds "owner" to body if isAdmin=false', { + callWithRequestImpls: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], + isAdmin: false + }, + asserts: { + callWithRequests: [ + ['shield.invalidateAPIKey', { + body: { + id: 'si8If24B1bKsmSLTAhJV', + owner: true, + }, + }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + errors: [], + }, + }, + }); + + postInvalidateTest('returns only successful invalidation requests', { + callWithRequestImpls: [ + async () => null, + async () => { + throw Boom.notAcceptable('test not acceptable message'); + }], + payload: { + apiKeys: [ + { id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }, + { id: 'ab8If24B1bKsmSLTAhNC', name: 'my-api-key2' } + ], + isAdmin: true + }, + asserts: { + callWithRequests: [ + ['shield.invalidateAPIKey', { + body: { + id: 'si8If24B1bKsmSLTAhJV', + }, + }], + ['shield.invalidateAPIKey', { + body: { + id: 'ab8If24B1bKsmSLTAhNC', + }, + }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }], + errors: [{ + id: 'ab8If24B1bKsmSLTAhNC', + name: 'my-api-key2', + error: Boom.notAcceptable('test not acceptable message'), + }] + }, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js new file mode 100644 index 0000000000000..2a6f935e00595 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { initCheckPrivilegesApi } from './privileges'; +import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; + +const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); + +describe('GET privileges', () => { + const getPrivilegesTest = ( + description, + { + preCheckLicenseImpl = () => null, + callWithRequestImpls = [], + asserts, + } + ) => { + test(description, async () => { + const mockServer = createMockServer(); + const pre = jest.fn().mockImplementation(preCheckLicenseImpl); + const mockCallWithRequest = jest.fn(); + + for (const impl of callWithRequestImpls) { + mockCallWithRequest.mockImplementationOnce(impl); + } + + initCheckPrivilegesApi(mockServer, mockCallWithRequest, pre); + + const headers = { + authorization: 'foo', + }; + + const request = { + method: 'GET', + url: `${INTERNAL_API_BASE_PATH}/api_key/privileges`, + headers, + }; + + const { result, statusCode } = await mockServer.inject(request); + + expect(pre).toHaveBeenCalled(); + + if (asserts.callWithRequests) { + for (const args of asserts.callWithRequests) { + expect(mockCallWithRequest).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }), + ...args + ); + } + } else { + expect(mockCallWithRequest).not.toHaveBeenCalled(); + } + + expect(statusCode).toBe(asserts.statusCode); + expect(result).toEqual(asserts.result); + }); + }; + + describe('failure', () => { + getPrivilegesTest('returns result of routePreCheckLicense', { + preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), + asserts: { + statusCode: 403, + result: { + error: 'Forbidden', + statusCode: 403, + message: 'test forbidden message', + }, + }, + }); + + getPrivilegesTest('returns error from first callWithRequest', { + callWithRequestImpls: [async () => { + throw Boom.notAcceptable('test not acceptable message'); + }, async () => { }], + asserts: { + callWithRequests: [ + ['shield.hasPrivileges', { + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + ], + }, + }], + ['shield.getAPIKeys', { owner: true }], + ], + statusCode: 406, + result: { + error: 'Not Acceptable', + statusCode: 406, + message: 'test not acceptable message', + }, + }, + }); + + getPrivilegesTest('returns error from second callWithRequest', { + callWithRequestImpls: [async () => { }, async () => { + throw Boom.notAcceptable('test not acceptable message'); + }], + asserts: { + callWithRequests: [ + ['shield.hasPrivileges', { + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + ], + }, + }], + ['shield.getAPIKeys', { owner: true }], + ], + statusCode: 406, + result: { + error: 'Not Acceptable', + statusCode: 406, + message: 'test not acceptable message', + }, + }, + }); + }); + + describe('success', () => { + getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { + callWithRequestImpls: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {} + }), + async () => ( + { + api_keys: + [{ + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }] + } + ), + ], + asserts: { + callWithRequests: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + ], + }, + }], + ], + statusCode: 200, + result: { + areApiKeysEnabled: true, + isAdmin: true, + }, + }, + }); + + getPrivilegesTest('returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', { + callWithRequestImpls: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {} + }), + async () => { + throw Boom.unauthorized('api keys are not enabled'); + }, + ], + asserts: { + callWithRequests: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + ], + }, + }], + ], + statusCode: 200, + result: { + areApiKeysEnabled: false, + isAdmin: true, + }, + }, + }); + + getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { + callWithRequestImpls: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false }, + index: {}, + application: {} + }), + async () => ( + { + api_keys: + [{ + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved' + }] + } + ), + ], + asserts: { + callWithRequests: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + ], + }, + }], + ], + statusCode: 200, + result: { + areApiKeysEnabled: true, + isAdmin: false, + }, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index e372fb02a8c44..e5d1fc83dac26 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -15,7 +15,10 @@ export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; +export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts index 132242606d88c..7a6c7f71bc98c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOGOUT } from '../urls'; - export const logout = (): null => { - cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`); + cy.request({ + method: 'GET', + url: `${Cypress.config().baseUrl}/logout`, + }).then(response => { + expect(response.status).to.eq(200); + }); return null; }; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts index 0bcd034a24cee..8fa1a03840e3b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts @@ -47,5 +47,5 @@ export const assertAtLeastOneEventMatchesSearch = () => export const toggleFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT, { timeout: DEFAULT_TIMEOUT }) .first() - .click(); + .click({ force: true }); }; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts index 14f06b55fd5ee..7dc98072b52f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts @@ -32,8 +32,7 @@ export const SEARCH_OR_FILTER_CONTAINER = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; /** Expands or collapses an event in the timeline */ -export const TOGGLE_TIMELINE_EXPAND_EVENT = - '[data-test-subj="timeline"] [data-test-subj="expand-event"]'; +export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; /** The body of the timeline flyout */ export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts index 400f82bf81188..57a1f318a7e31 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts @@ -57,7 +57,7 @@ describe('Events Viewer', () => { .should('eq', 'Customize Columns'); }); - it.skip('closes the fields browser when the user clicks outside of it', () => { + it('closes the fields browser when the user clicks outside of it', () => { openEventsViewerFieldsBrowser(); clickOutsideFieldsBrowser(); @@ -81,7 +81,7 @@ describe('Events Viewer', () => { ); }); - it.skip('removes the message field from the timeline when the user un-checks the field', () => { + it('removes the message field from the timeline when the user un-checks the field', () => { const toggleField = 'message'; cy.get(`${EVENTS_VIEWER_PANEL} [data-test-subj="header-text-${toggleField}"]`).should('exist'); @@ -99,7 +99,7 @@ describe('Events Viewer', () => { ); }); - it.skip('filters the events by applying filter criteria from the search bar at the top of the page', () => { + it('filters the events by applying filter criteria from the search bar at the top of the page', () => { const filterInput = '4bf34c1c-eaa9-46de-8921-67a4ccc49829'; // this will never match real data cy.get(HEADER_SUBTITLE) @@ -119,7 +119,7 @@ describe('Events Viewer', () => { }); }); - it.skip('adds a field to the events viewer when the user clicks the checkbox', () => { + it('adds a field to the events viewer when the user clicks the checkbox', () => { const filterInput = 'host.geo.c'; const toggleField = 'host.geo.city_name'; @@ -158,7 +158,7 @@ describe('Events Viewer', () => { }); }); - it.skip('launches the inspect query modal when the inspect button is clicked', () => { + it('launches the inspect query modal when the inspect button is clicked', () => { // wait for data to load cy.get(HEADER_SUBTITLE) .invoke('text') @@ -171,7 +171,7 @@ describe('Events Viewer', () => { cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('exist'); }); - it.skip('resets all fields in the events viewer when `Reset Fields` is clicked', () => { + it('resets all fields in the events viewer when `Reset Fields` is clicked', () => { const filterInput = 'host.geo.c'; const toggleField = 'host.geo.city_name'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index cec5d6d88d86d..a03ff0c1845f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -28,7 +28,7 @@ describe('ml conditional links', () => { return logout(); }); - it.skip('sets the KQL from a single IP with a value for the query', () => { + it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -37,7 +37,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a multiple IPs with a null for the query', () => { + it('sets the KQL from a multiple IPs with a null for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -46,7 +46,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a multiple IPs with a value for the query', () => { + it('sets the KQL from a multiple IPs with a value for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -55,7 +55,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a $ip$ with a value for the query', () => { + it('sets the KQL from a $ip$ with a value for the query', () => { loginAndWaitForPage(mlNetworkKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -64,7 +64,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a single host name with a value for query', () => { + it('sets the KQL from a single host name with a value for query', () => { loginAndWaitForPage(mlHostSingleHostKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -73,7 +73,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a multiple host names with null for query', () => { + it('sets the KQL from a multiple host names with null for query', () => { loginAndWaitForPage(mlHostMultiHostNullKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -82,7 +82,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a multiple host names with a value for query', () => { + it('sets the KQL from a multiple host names with a value for query', () => { loginAndWaitForPage(mlHostMultiHostKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -91,7 +91,7 @@ describe('ml conditional links', () => { ); }); - it.skip('sets the KQL from a undefined/null host name but with a value for query', () => { + it('sets the KQL from a undefined/null host name but with a value for query', () => { loginAndWaitForPage(mlHostVariableHostKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( 'have.attr', @@ -100,7 +100,7 @@ describe('ml conditional links', () => { ); }); - it.skip('redirects from a single IP with a null for the query', () => { + it('redirects from a single IP with a null for the query', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', @@ -108,7 +108,7 @@ describe('ml conditional links', () => { ); }); - it.skip('redirects from a single IP with a value for the query', () => { + it('redirects from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', @@ -116,7 +116,7 @@ describe('ml conditional links', () => { ); }); - it.skip('redirects from a multiple IPs with a null for the query', () => { + it('redirects from a multiple IPs with a null for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 3f6ed86f29285..8c2902fd804ac 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -23,7 +23,7 @@ describe('toggle column in timeline', () => { const timestampField = '@timestamp'; const idField = '_id'; - it.skip('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { + it('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -39,7 +39,7 @@ describe('toggle column in timeline', () => { ); }); - it.skip('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { + it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -50,14 +50,14 @@ describe('toggle column in timeline', () => { cy.get( `[data-test-subj="timeline"] [data-test-subj="toggle-field-${timestampField}"]` - ).uncheck(); + ).uncheck({ force: true }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${timestampField}"]`).should( 'not.exist' ); }); - it.skip('adds the _id field to the timeline when the user checks the field', () => { + it('adds the _id field to the timeline when the user checks the field', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -66,7 +66,9 @@ describe('toggle column in timeline', () => { 'not.exist' ); - cy.get(`[data-test-subj="timeline"] [data-test-subj="toggle-field-${idField}"]`).check(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="toggle-field-${idField}"]`).check({ + force: true, + }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${idField}"]`).should('exist'); }); diff --git a/x-pack/legacy/plugins/siem/index.test.ts b/x-pack/legacy/plugins/siem/index.test.ts index 702676cc202d5..5b7c488eb174c 100644 --- a/x-pack/legacy/plugins/siem/index.test.ts +++ b/x-pack/legacy/plugins/siem/index.test.ts @@ -4,27 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ -import { siem } from '.'; +import { getRequiredPlugins } from '.'; +// This test is a temporary test which is so we do not accidentally check-in +// feature flags turned on from "alerting" and "actions". If those get +// turned on during a check-in it will cause everyone's Kibana to not start. +// Once alerting and actions are part of the plugins by default this test +// should be removed. describe('siem plugin tests', () => { - // This test is a temporary test which is so we do not accidentally check-in - // feature flags turned on from "alerting" and "actions". If those get - // turned on during a check-in it will cause everyone's Kibana to not start. - // Once alerting and actions are part of the plugins by default this test - // should be removed. - test(` - You have accidentally tried to check-in a feature flag with alerting located - here: x-pack/legacy/plugins/siem/index.ts, please change the plugin require to - NOT have these two inside of the require array." - `, () => { - class MockPlugin { - require: string[]; - constructor({ require }: { require: string[] }) { - this.require = require; - } - } - const plugin = siem({ Plugin: MockPlugin }); - expect(plugin.require.includes('alerting')).toBe(false); - expect(plugin.require.includes('actions')).toBe(false); + describe('getRequiredPlugins', () => { + test('null settings returns regular kibana and elasticsearch plugins', () => { + expect(getRequiredPlugins(null, null)).toEqual(['kibana', 'elasticsearch']); + }); + + test('undefined settings returns regular kibana and elasticsearch plugins', () => { + expect(getRequiredPlugins(undefined, undefined)).toEqual(['kibana', 'elasticsearch']); + }); + + test('alertingFeatureEnabled being false returns regular kibana and elasticsearch plugins', () => { + expect(getRequiredPlugins('false', undefined)).toEqual(['kibana', 'elasticsearch']); + }); + + test('alertingFeatureEnabled being true returns action and alerts', () => { + expect(getRequiredPlugins('true', undefined)).toEqual([ + 'kibana', + 'elasticsearch', + 'alerting', + 'actions', + ]); + }); + + test('alertingFeatureEnabled being false but a string for siemIndex returns alerting and actions', () => { + expect(getRequiredPlugins('false', '.siem-signals-frank')).toEqual([ + 'kibana', + 'elasticsearch', + 'alerting', + 'actions', + ]); + }); + + test('alertingFeatureEnabled being true and a string for siemIndex returns alerting and actions', () => { + expect(getRequiredPlugins('true', '.siem-signals-frank')).toEqual([ + 'kibana', + 'elasticsearch', + 'alerting', + 'actions', + ]); + }); + + test('alertingFeatureEnabled being true and an empty string for siemIndex returns regular kibana and elasticsearch plugins', () => { + expect(getRequiredPlugins(undefined, '')).toEqual(['kibana', 'elasticsearch']); + }); + + test('alertingFeatureEnabled being true and a string of spaces for siemIndex returns regular kibana and elasticsearch plugins', () => { + expect(getRequiredPlugins(undefined, ' ')).toEqual(['kibana', 'elasticsearch']); + }); + + test('alertingFeatureEnabled being null and a string for siemIndex returns alerting and actions', () => { + expect(getRequiredPlugins(null, '.siem-signals-frank')).toEqual([ + 'kibana', + 'elasticsearch', + 'alerting', + 'actions', + ]); + }); + + test('alertingFeatureEnabled being undefined and a string for siemIndex returns alerting and actions', () => { + expect(getRequiredPlugins(undefined, '.siem-signals-frank')).toEqual([ + 'kibana', + 'elasticsearch', + 'alerting', + 'actions', + ]); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 45dc88b15c159..06aaec631be66 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -23,21 +23,41 @@ import { DEFAULT_INTERVAL_VALUE, DEFAULT_FROM, DEFAULT_TO, + DEFAULT_SIGNALS_INDEX, + DEFAULT_SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; +// This is VERY TEMPORARY as we need a way to turn on alerting and actions +// for the server without having to manually edit this file. Once alerting +// and actions has their enabled true by default this can be removed. +// 'alerting', 'actions' are hidden behind feature flags at the moment so if you turn +// these on without the feature flags turned on then Kibana will crash since we are a legacy plugin +// and legacy plugins cannot have optional requirements. +// This returns ['alerting', 'actions', 'kibana', 'elasticsearch'] iff alertingFeatureEnabled is true +// or if the developer signalsIndex is setup. Otherwise this returns ['kibana', 'elasticsearch'] +export const getRequiredPlugins = ( + alertingFeatureEnabled: string | null | undefined, + signalsIndex: string | null | undefined +) => { + const baseRequire = ['kibana', 'elasticsearch']; + if ( + (signalsIndex != null && signalsIndex.trim() !== '') || + (alertingFeatureEnabled && alertingFeatureEnabled.toLowerCase() === 'true') + ) { + return [...baseRequire, 'alerting', 'actions']; + } else { + return baseRequire; + } +}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const siem = (kibana: any) => { return new kibana.Plugin({ id: APP_ID, configPrefix: 'xpack.siem', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch'], - // Uncomment these lines to turn on alerting and action for detection engine and comment the other - // require statement out. These are hidden behind feature flags at the moment so if you turn - // these on without the feature flags turned on then Kibana will crash since we are a legacy plugin - // and legacy plugins cannot have optional requirements. - // require: ['kibana', 'elasticsearch', 'alerting', 'actions'], + require: getRequiredPlugins(process.env.ALERTING_FEATURE_ENABLED, process.env.SIGNALS_INDEX), uiExports: { app: { description: i18n.translate('xpack.siem.securityDescription', { @@ -106,6 +126,18 @@ export const siem = (kibana: any) => { category: ['siem'], requiresPageReload: true, }, + [DEFAULT_SIGNALS_INDEX_KEY]: { + name: i18n.translate('xpack.siem.uiSettings.defaultSignalsIndexLabel', { + defaultMessage: 'Elasticsearch signals index', + }), + value: DEFAULT_SIGNALS_INDEX, + description: i18n.translate('xpack.siem.uiSettings.defaultSignalsIndexDescription', { + defaultMessage: + '

Elasticsearch signals index from which outputted signals will appear by default

', + }), + category: ['siem'], + requiresPageReload: true, + }, [DEFAULT_ANOMALY_SCORE]: { name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { defaultMessage: 'Anomaly threshold', diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 0e01921325222..d239961ee75d7 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -7,27 +7,16 @@ "scripts": { "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open", - "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/mocha-multi-reporters --reporter-options configFile=./reporter_config.json; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/" + "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;" }, "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^10.0.1" + "@types/react-beautiful-dnd": "^11.0.3" }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^10.0.1", + "react-beautiful-dnd": "^12.1.1", "react-markdown": "^4.0.6" - }, - "workspaces": { - "packages": [ - "packages/*" - ], - "nohoist": [ - "**/mochawesome", - "**/mochawesome/**", - "**/mocha-multi-reporters", - "**/mocha-multi-reporters/**" - ] } } diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index b71c4fe699860..73f9b65ba3546 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -8,8 +8,8 @@ import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start( - npStart.core, - npStart.plugins -); +new Plugin( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { opaqueId: Symbol('siem'), env: {} as any, config: { get: () => ({} as any) } }, + chrome +).start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx new file mode 100644 index 0000000000000..2337f2cd7512a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx @@ -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 React from 'react'; + +import { MatrixHistogramBasicProps } from '../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { MatrixHistogram } from '../matrix_histogram'; +import * as i18n from './translation'; + +export const AnomaliesOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { + const dataKey = 'anomaliesOverTime'; + const { totalCount } = props; + const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`; + const { ...matrixOverTimeProps } = props; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts new file mode 100644 index 0000000000000..f28a7176fd09d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ANOMALIES_COUNT_FREQUENCY_BY_ACTION = i18n.translate( + 'xpack.siem.anomaliesOverTime.anomaliesCountFrequencyByJobTile', + { + defaultMessage: 'Anomalies count by job', + } +); + +export const SHOWING = i18n.translate('xpack.siem.anomaliesOverTime.showing', { + defaultMessage: 'Showing', +}); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.anomaliesOverTime.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {anomaly} other {anomalies}}`, + }); diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx index 408743d261797..124ef26602f35 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx @@ -307,7 +307,7 @@ const withUnfocused = (state: AutocompleteFieldState) => ({ isFocused: false, }); -export const FixedEuiFieldSearch: React.SFC & +export const FixedEuiFieldSearch: React.FC & EuiFieldSearchProps & { inputRef?: (element: HTMLInputElement | null) => void; onSearch: (value: string) => void; @@ -319,10 +319,10 @@ const AutocompleteContainer = euiStyled.div` AutocompleteContainer.displayName = 'AutocompleteContainer'; -const SuggestionsPanel = euiStyled(EuiPanel).attrs({ +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ paddingSize: 'none', hasShadow: true, -})` +}))` position: absolute; width: 100%; margin-top: 2px; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index 6347b93772b4e..d51f5e081468c 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -127,7 +127,7 @@ export const AreaChart = React.memo<{ return checkIfAnyValidSeriesExist(areaChart) ? ( {({ measureRef, content: { height, width } }) => ( - + {({ measureRef, content: { height, width } }) => ( - + { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 1f892acef7ef3..03a04983f9f86 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -12,11 +12,7 @@ exports[`UtilityBar it renders 1`] = ` - Test popover -

- } + popoverContent={[Function]} > Test action
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index bf13a503838cf..27688ec24530e 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -7,7 +7,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import '../../../mock/ui_settings'; @@ -33,7 +32,7 @@ describe('UtilityBar', () => {
- {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
@@ -61,7 +60,7 @@ describe('UtilityBar', () => { - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
@@ -91,7 +90,7 @@ describe('UtilityBar', () => { - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index 7a1c35183e503..f71bdfda705d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import '../../../mock/ui_settings'; @@ -29,7 +28,7 @@ describe('UtilityBarAction', () => { test('it renders a popover', () => { const wrapper = mount( - {'Test popover'}

}> +

{'Test popover'}

}> {'Test action'}
diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index ae4362bdbcd7b..2ad48bc9b9c92 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -5,7 +5,7 @@ */ import { EuiPopover } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { LinkIcon, LinkIconProps } from '../../link_icon'; import { BarAction } from './styles'; @@ -14,6 +14,8 @@ const Popover = React.memo( ({ children, color, iconSide, iconSize, iconType, popoverContent }) => { const [popoverState, setPopoverState] = useState(false); + const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]); + return ( ( closePopover={() => setPopoverState(false)} isOpen={popoverState} > - {popoverContent} + {popoverContent?.(closePopover)} ); } @@ -38,7 +40,7 @@ const Popover = React.memo( Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { - popoverContent?: React.ReactNode; + popoverContent?: (closePopover: () => void) => React.ReactNode; } export const UtilityBarAction = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx new file mode 100644 index 0000000000000..ee9533341a4f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40309 + +import { MovementMode, DraggableId } from 'react-beautiful-dnd'; + +export interface BeforeCapture { + draggableId: DraggableId; + mode: MovementMode; +} diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index c513f7a451240..a3528158a0317 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -6,10 +6,11 @@ import { defaultTo, noop } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; +import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; +import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; import { dragAndDropModel, dragAndDropSelectors } from '../../store'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; @@ -20,6 +21,7 @@ import { addProviderToTimeline, fieldWasDroppedOnTimelineColumns, IS_DRAGGING_CLASS_NAME, + IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, providerWasDroppedOnTimeline, providerWasDroppedOnTimelineButton, draggableIsField, @@ -75,11 +77,16 @@ export const DragDropContextWrapperComponent = React.memo( if (!draggableIsField(result)) { document.body.classList.remove(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(result)) { + document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }, [browserFields, dataProviders] ); return ( - + // @ts-ignore + {children} ); @@ -107,7 +114,7 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); -const onDragStart = (initial: DragStart) => { +const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined ? window.pageXOffset @@ -120,9 +127,13 @@ const onDragStart = (initial: DragStart) => { window.onscroll = () => window.scrollTo(x, y); - if (!draggableIsField(initial)) { + if (!draggableIsField(before)) { document.body.classList.add(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(before)) { + document.body.classList.add(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }; const enableScrolling = () => (window.onscroll = () => noop); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 35d54414944f4..c314785511201 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -34,36 +34,52 @@ export const useDraggablePortalContext = () => useContext(DraggablePortalContext const Wrapper = styled.div` display: inline-block; max-width: 100%; + + [data-rbd-placeholder-context-id] { + display: none !important; + } `; Wrapper.displayName = 'Wrapper'; const ProviderContainer = styled.div<{ isDragging: boolean }>` - ${({ theme, isDragging }) => css` - &, - &::before, - &::after { - transition: background ${theme.eui.euiAnimSpeedFast} ease, - color ${theme.eui.euiAnimSpeedFast} ease; - } - - ${!isDragging && - ` + &, + &::before, + &::after { + transition: background ${({ theme }) => theme.eui.euiAnimSpeedFast} ease, + color ${({ theme }) => theme.eui.euiAnimSpeedFast} ease; + } + + ${({ isDragging }) => + !isDragging && + css` & { border-radius: 2px; padding: 0 4px 0 8px; position: relative; - z-index: ${theme.eui.euiZLevel0} !important; + z-index: ${({ theme }) => theme.eui.euiZLevel0} !important; &::before { background-image: linear-gradient( 135deg, - ${theme.eui.euiColorMediumShade} 25%, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25% ), - linear-gradient(-135deg, ${theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%); + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorMediumShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorMediumShade} 75% + ); background-position: 0 0, 1px 0, 1px -1px, 0px 1px; background-size: 2px 2px; bottom: 2px; @@ -87,17 +103,29 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &, tr:hover & { - background-color: ${theme.eui.euiColorLightShade}; + background-color: ${({ theme }) => theme.eui.euiColorLightShade}; &::before { background-image: linear-gradient( 135deg, - ${theme.eui.euiColorDarkShade} 25%, + ${({ theme }) => theme.eui.euiColorDarkShade} 25%, transparent 25% ), - linear-gradient(-135deg, ${theme.eui.euiColorDarkShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorDarkShade} 75%), - linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorDarkShade} 75%); + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorDarkShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorDarkShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorDarkShade} 75% + ); } } @@ -107,34 +135,46 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` .${STATEFUL_EVENT_CSS_CLASS_NAME}:focus &:focus, tr:hover &:hover, tr:hover &:focus { - background-color: ${theme.eui.euiColorPrimary}; + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; &, & a, & a:hover { - color: ${theme.eui.euiColorEmptyShade}; + color: ${({ theme }) => theme.eui.euiColorEmptyShade}; } &::before { background-image: linear-gradient( 135deg, - ${theme.eui.euiColorEmptyShade} 25%, + ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, transparent 25% ), - linear-gradient(-135deg, ${theme.eui.euiColorEmptyShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorEmptyShade} 75%), - linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorEmptyShade} 75%); + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorEmptyShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorEmptyShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorEmptyShade} 75% + ); } } `} - ${isDragging && - ` + ${({ isDragging }) => + isDragging && + css` & { z-index: 9999 !important; } `} - `} `; ProviderContainer.displayName = 'ProviderContainer'; @@ -192,7 +232,7 @@ const DraggableWrapperComponent = React.memo( ( {(provided, snapshot) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts index 8d3334b05bfaf..af4b9b280f3cd 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts @@ -116,7 +116,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( draggableIsContent({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -230,10 +230,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineProviders({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -286,10 +286,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns returns false when the destination is null', () => { + test('it returns returns false when the destination is undefined', () => { expect( destinationIsTimelineColumns({ - destination: null, + destination: undefined, draggableId: getDraggableFieldId({ contextId: 'test', fieldId: 'event.action' }), reason: 'DROP', source: { @@ -342,10 +342,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineButton({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -436,10 +436,10 @@ describe('helpers', () => { ).toEqual('timeline'); }); - test('it returns returns an empty string when the destination is null', () => { + test('it returns returns an empty string when the destination is undefined', () => { expect( getTimelineIdFromDestination({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -558,7 +558,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( providerWasDroppedOnTimeline({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index 415970474db4c..ae3a8828491e3 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -224,3 +224,6 @@ export const DRAG_TYPE_FIELD = 'drag-type-field'; /** This class is added to the document body while dragging */ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +/** This class is added to the document body while timeline field dragging */ +export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index faf65338b4337..5bff59494b9ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -7,19 +7,17 @@ import { rgba } from 'polished'; import * as React from 'react'; import { pure } from 'recompose'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; const Field = styled.div` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorEmptyShade}; - border: ${theme.eui.euiBorderThin}; - box-shadow: 0 2px 2px -1px ${rgba(theme.eui.euiColorMediumShade, 0.3)}, - 0 1px 5px -2px ${rgba(theme.eui.euiColorMediumShade, 0.3)}; - font-size: ${theme.eui.euiFontSizeXS}; - font-weight: ${theme.eui.euiFontWeightSemiBold}; - line-height: ${theme.eui.euiLineHeight}; - padding: ${theme.eui.paddingSizes.xs}; - `} + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border: ${({ theme }) => theme.eui.euiBorderThin}; + box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, + 0 1px 5px -2px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; `; Field.displayName = 'Field'; diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index ec6646fc76085..18b271a3abc29 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -18,7 +18,7 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useEffect, useState, useCallback } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { OnDataProviderEdited } from '../timeline/events'; @@ -46,11 +46,10 @@ export const HeaderContainer = styled.div` HeaderContainer.displayName = 'HeaderContainer'; -// SIDE EFFECT: the following `injectGlobal` overrides the default styling +// SIDE EFFECT: the following `createGlobalStyle` overrides the default styling // of euiComboBoxOptionsList because it's implemented as a popover, so it's // not selectable as a child of the styled component -// eslint-disable-next-line no-unused-expressions -injectGlobal` +const StatefulEditDataProviderGlobalStyle = createGlobalStyle` .euiComboBoxOptionsList { z-index: 9999; } @@ -158,104 +157,107 @@ export const StatefulEditDataProvider = React.memo( }, []); return ( - - - - - - - 0 ? updatedField[0].label : null}> + <> + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + - - - + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - + - - - - - - + ) : null} - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - + - ) : null} - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx index dbd9e3f763f92..f2abfdf307fa3 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx @@ -6,15 +6,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; -const Header = styled.header.attrs({ - className: 'siemEmbeddable__header', -})` - ${({ theme }) => css` - border-bottom: ${theme.eui.euiBorderThin}; - padding: ${theme.eui.paddingSizes.m}; - `} +const Header = styled.header.attrs(({ className }) => ({ + className: `siemEmbeddable__header ${className}`, +}))` + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + padding: ${({ theme }) => theme.eui.paddingSizes.m}; `; Header.displayName = 'Header'; 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 794e26b417012..fa3b95a2c805b 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 @@ -34,41 +34,40 @@ interface EmbeddableMapProps { maintainRatio?: boolean; } -const EmbeddableMap = styled.div.attrs({ +const EmbeddableMap = styled.div.attrs(() => ({ className: 'siemEmbeddable__map', -})` - ${({ maintainRatio, theme }) => css` - .embPanel { - border: none; - box-shadow: none; - } - - .mapToolbarOverlay__button { - display: none; - } - - ${maintainRatio && - css` - padding-top: calc(3 / 4 * 100%); //4:3 (standard) ratio - position: relative; - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { - padding-top: calc(9 / 32 * 100%); //32:9 (ultra widescreen) ratio - } +}))` + .embPanel { + border: none; + box-shadow: none; + } - @media only screen and (min-width: 1441px) and (min-height: 901px) { - padding-top: calc(9 / 21 * 100%); //21:9 (ultrawide) ratio - } + .mapToolbarOverlay__button { + display: none; + } - .embPanel { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - } - `} - `} + ${({ maintainRatio }) => + maintainRatio && + css` + padding-top: calc(3 / 4 * 100%); //4:3 (standard) ratio + position: relative; + + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + padding-top: calc(9 / 32 * 100%); //32:9 (ultra widescreen) ratio + } + + @media only screen and (min-width: 1441px) and (min-height: 901px) { + padding-top: calc(9 / 21 * 100%); //21:9 (ultrawide) ratio + } + + .embPanel { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + } + `} `; EmbeddableMap.displayName = 'EmbeddableMap'; 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 2ad38bae3ccef..a2c3e2cc288d7 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 @@ -14,6 +14,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); jest.mock('uuid', () => { return { + v1: jest.fn(() => '27261ae0-0bbb-11ea-b0ea-db767b07ea47'), v4: jest.fn(() => '9e1f72a9-7c73-4b7f-a562-09940f7daf4a'), }; }); diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx index 6c4bc797d39f8..6233fcfe7c823 100644 --- a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx @@ -30,7 +30,7 @@ describe('Error Toast Dispatcher', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(ErrorToastDispatcherComponent)'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index c5c0fe3503561..c868b7950289a 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -100,7 +100,7 @@ export const EventsViewer = React.memo( {({ measureRef, content: { width = 0 } }) => ( <> - +
` - ${({ theme, width }) => css` - background-color: ${theme.eui.euiColorLightestShade}; - border: ${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiColorMediumShade}; - border-radius: ${theme.eui.euiBorderRadius}; - left: 0; - padding: ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.m}; - position: absolute; - top: calc(100% + ${theme.eui.euiSize}); - width: ${width}px; - z-index: 9990; - `} + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid + ${({ theme }) => theme.eui.euiColorMediumShade}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + left: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s} + ${({ theme }) => theme.eui.paddingSizes.m}; + position: absolute; + top: calc(100% + ${({ theme }) => theme.eui.euiSize}); + width: ${({ width }) => width}px; + z-index: 9990; `; FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx index 9c2cf2cb0e0b2..d5d8c54775566 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx @@ -16,7 +16,7 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useContext } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; @@ -30,51 +30,55 @@ import * as i18n from './translations'; * The name of a (draggable) field */ export const FieldNameContainer = styled.span` - ${({ theme }) => css` - padding: 5px; - { - border-radius: 4px; - padding: 0 4px 0 8px; - position: relative; + padding: 5px; + { + border-radius: 4px; + padding: 0 4px 0 8px; + position: relative; + + &::before { + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, + transparent 25% + ), + linear-gradient(-135deg, ${({ theme }) => + theme.eui.euiColorMediumShade} 25%, transparent 25%), + linear-gradient(135deg, transparent 75%, ${({ theme }) => + theme.eui.euiColorMediumShade} 75%), + linear-gradient(-135deg, transparent 75%, ${({ theme }) => + theme.eui.euiColorMediumShade} 75%); + background-position: 0 0, 1px 0, 1px -1px, 0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; + } + + &:hover, + &:focus { + transition: background-color 0.7s ease; + background-color: #000; + color: #fff; &::before { background-image: linear-gradient( 135deg, - ${theme.eui.euiColorMediumShade} 25%, + #fff 25%, transparent 25% ), - linear-gradient(-135deg, ${theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; + linear-gradient(-135deg, ${({ theme }) => + theme.eui.euiColorLightestShade} 25%, transparent 25%), + linear-gradient(135deg, transparent 75%, ${({ theme }) => + theme.eui.euiColorLightestShade} 75%), + linear-gradient(-135deg, transparent 75%, ${({ theme }) => + theme.eui.euiColorLightestShade} 75%); } - - &:hover, - &:focus { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - - &::before { - background-image: linear-gradient( - 135deg, - #fff 25%, - transparent 25% - ), - linear-gradient(-135deg, ${theme.eui.euiColorLightestShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorLightestShade} 75%), - linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorLightestShade} 75%); - } - } - `} + } `; FieldNameContainer.displayName = 'FieldNameContainer'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 8948f765b8fbc..e943ca6f3e863 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -6,7 +6,6 @@ import { mount } from 'enzyme'; import * as React from 'react'; -import 'jest-styled-components'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index 892c160d054bd..bdda8497a8bcb 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -18,25 +18,23 @@ const disableSticky = 'screen and (max-width: ' + euiLightVars.euiBreakpoints.s const disableStickyMq = window.matchMedia(disableSticky); const Wrapper = styled.aside<{ isSticky?: boolean }>` - ${props => css` - position: relative; - z-index: ${props.theme.eui.euiZNavigation}; - background: ${props.theme.eui.euiColorEmptyShade}; - border-bottom: ${props.theme.eui.euiBorderThin}; - padding: ${props.theme.eui.paddingSizes.m} ${gutterTimeline} ${ - props.theme.eui.paddingSizes.m - } ${props.theme.eui.paddingSizes.l}; - - ${props.isSticky && - ` + position: relative; + z-index: ${({ theme }) => theme.eui.euiZNavigation}; + background: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} ${({ theme }) => + theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; + + ${({ isSticky }) => + isSticky && + css` top: ${offsetChrome}px !important; `} - @media only ${disableSticky} { - position: static !important; - z-index: ${props.theme.eui.euiZContent} !important; - } - `} + @media only ${disableSticky} { + position: static !important; + z-index: ${({ theme }) => theme.eui.euiZContent} !important; + } `; Wrapper.displayName = 'Wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 4b7b935b1f536..65233e55901ff 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { flyoutHeaderHeight } from '../'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index 168cacf3e97e1..53365a4daa34a 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -35,7 +35,7 @@ FlexItem.displayName = 'FlexItem'; interface HeaderGlobalProps { hideDetectionEngine?: boolean; } -export const HeaderGlobal = React.memo(({ hideDetectionEngine = true }) => ( +export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index ae33b63e93d39..9c50a915b7ba8 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -7,7 +7,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx index 4db2a35c600e9..9877372ff9f41 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBetaBadge, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { + EuiBetaBadge, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -14,6 +21,7 @@ import { Subtitle, SubtitleProps } from '../subtitle'; interface HeaderProps { border?: boolean; + isLoading?: boolean; } const Header = styled.header.attrs({ @@ -26,6 +34,9 @@ const Header = styled.header.attrs({ css` border-bottom: ${theme.eui.euiBorderThin}; padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } `} `} `; @@ -85,6 +96,7 @@ export const HeaderPage = React.memo( border, children, draggableArguments, + isLoading, subtitle, subtitle2, title, @@ -132,6 +144,7 @@ export const HeaderPage = React.memo( {subtitle && } {subtitle2 && } + {border && isLoading && } {children && ( diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index fffeece818d13..4a6da9c80968f 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -7,7 +7,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx index e46ae55a57a45..14af10eb6cd9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx @@ -15,19 +15,18 @@ interface HeaderProps { border?: boolean; } -const Header = styled.header.attrs({ +const Header = styled.header.attrs(() => ({ className: 'siemHeaderSection', -})` - ${({ border, theme }) => css` - margin-bottom: ${theme.eui.euiSizeL}; - user-select: text; +}))` + margin-bottom: ${({ theme }) => theme.eui.euiSizeL}; + user-select: text; - ${border && - css` - border-bottom: ${theme.eui.euiBorderThin}; - padding-bottom: ${theme.eui.paddingSizes.l}; - `} - `} + ${({ border }) => + border && + css` + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + padding-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; + `} `; Header.displayName = 'Header'; diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx deleted file mode 100644 index b59753e8add6a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton, EuiHorizontalRule, EuiIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import styled from 'styled-components'; - -export const Icon = styled(EuiIcon)` - margin-right: ${theme.euiSizeS}; -`; - -Icon.displayName = 'Icon'; - -export const HelpMenuComponent = React.memo(() => ( - <> - - - - - -
- - -
-
- - - - - - - -)); - -HelpMenuComponent.displayName = 'HelpMenuComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx index d145769319d5b..43fd8e653f3d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx @@ -4,20 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { render, unmountComponentAtNode } from 'react-dom'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { pure } from 'recompose'; import chrome from 'ui/chrome'; - -import { HelpMenuComponent } from './help_menu'; +import { i18n } from '@kbn/i18n'; export const HelpMenu = pure<{}>(() => { useEffect(() => { - chrome.helpExtension.set(domNode => { - render(, domNode); - return () => { - unmountComponentAtNode(domNode); - }; + chrome.helpExtension.set({ + appName: i18n.translate('xpack.siem.chrome.help.appName', { + defaultMessage: 'SIEM', + }), + links: [ + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/siem', + }, + ], }); }, []); diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx index 8e4387f35056e..451db49028ee1 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index 988fa4a677d7a..55628fe2e8d33 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -18,14 +18,14 @@ import { pure } from 'recompose'; import styled, { css } from 'styled-components'; const Aside = styled.aside<{ overlay?: boolean; overlayBackground?: string }>` - ${({ overlay, overlayBackground, theme }) => css` - padding: ${theme.eui.paddingSizes.m}; + padding: ${({ theme }) => theme.eui.paddingSizes.m}; - ${overlay && - ` - background: ${ - overlayBackground ? rgba(overlayBackground, 0.9) : rgba(theme.eui.euiColorEmptyShade, 0.9) - }; + ${({ overlay, overlayBackground, theme }) => + overlay && + css` + background: ${overlayBackground + ? rgba(overlayBackground, 0.9) + : rgba(theme.eui.euiColorEmptyShade, 0.9)}; bottom: 0; left: 0; position: absolute; @@ -33,22 +33,21 @@ const Aside = styled.aside<{ overlay?: boolean; overlayBackground?: string }>` top: 0; z-index: ${theme.eui.euiZLevel1}; `} - `} `; Aside.displayName = 'Aside'; -const FlexGroup = styled(EuiFlexGroup).attrs({ +const FlexGroup = styled(EuiFlexGroup).attrs(() => ({ alignItems: 'center', direction: 'column', gutterSize: 's', justifyContent: 'center', -})<{ overlay: { overlay?: boolean } }>` +}))<{ overlay: { overlay?: boolean } }>` ${({ overlay }) => overlay && - ` - height: 100%; - `} + css` + height: 100%; + `} `; FlexGroup.displayName = 'FlexGroup'; diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index 5b9cb48789739..42867c09b971b 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -7,11 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel, EuiText } from '@elastic/eui'; import * as React from 'react'; import { pure } from 'recompose'; -import styled, { injectGlobal } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; -// SIDE EFFECT: the following `injectGlobal` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -injectGlobal` +// SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly +const LoadingPanelGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } @@ -41,27 +40,30 @@ export const LoadingPanel = pure( position = 'relative', zIndex = 'inherit', }) => ( - - - - - - - + <> + + + + + + + - - {text} - - - - - + + {text} + + + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx index a1d12f0d4e29e..6319af3e6ffa1 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { MarkdownHint } from './markdown_hint'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx new file mode 100644 index 0000000000000..bdd8a0c544ed8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { shallow } from 'enzyme'; +import * as React from 'react'; + +import { MatrixHistogram } from '.'; + +jest.mock('@elastic/eui', () => { + return { + EuiPanel: (children: JSX.Element) => <>{children}, + EuiLoadingContent: () =>
, + }; +}); + +jest.mock('../loader', () => { + return { + Loader: () =>
, + }; +}); + +jest.mock('../../lib/settings/use_kibana_ui_setting', () => { + return { useKibanaUiSetting: () => [false] }; +}); + +jest.mock('../header_section', () => { + return { + HeaderSection: () =>
, + }; +}); + +jest.mock('../charts/barchart', () => { + return { + BarChart: () =>
, + }; +}); + +describe('Load More Events Table Component', () => { + const mockMatrixOverTimeHistogramProps = { + data: [], + dataKey: 'mockDataKey', + endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), + id: 'mockId', + loading: true, + updateDateRange: () => {}, + startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), + subtitle: 'mockSubtitle', + totalCount: -1, + title: 'mockTitle', + }; + describe('rendering', () => { + test('it renders EuiLoadingContent on initialLoad', () => { + const wrapper = shallow(); + + expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy(); + }); + + test('it renders Loader while fetching data if visited before', () => { + const mockProps = { + ...mockMatrixOverTimeHistogramProps, + data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], + totalCount: 10, + loading: true, + }; + const wrapper = shallow(); + expect(wrapper.find('.loader')).toBeTruthy(); + }); + + test('it renders BarChart if data available', () => { + const mockProps = { + ...mockMatrixOverTimeHistogramProps, + data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], + totalCount: 10, + loading: false, + }; + const wrapper = shallow(); + + expect(wrapper.find(`.barchart`)).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx new file mode 100644 index 0000000000000..f79c61a29c26b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; +import { ScaleType } from '@elastic/charts'; + +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiLoadingContent } from '@elastic/eui'; +import { BarChart } from '../charts/barchart'; +import { HeaderSection } from '../header_section'; +import { ChartSeriesData } from '../charts/common'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; +import { Loader } from '../loader'; +import { Panel } from '../panel'; +import { getBarchartConfigs, getCustomChartData } from './utils'; +import { MatrixHistogramProps, MatrixHistogramDataTypes } from './types'; + +export const MatrixHistogram = ({ + data, + dataKey, + endDate, + id, + loading, + mapping, + scaleType = ScaleType.Time, + startDate, + subtitle, + title, + totalCount, + updateDateRange, + yTickFormatter, + showLegend, +}: MatrixHistogramProps) => { + const barchartConfigs = getBarchartConfigs({ + from: startDate, + to: endDate, + onBrushEnd: updateDateRange, + scaleType, + yTickFormatter, + showLegend, + }); + const [showInspect, setShowInspect] = useState(false); + const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE); + const [loadingInitial, setLoadingInitial] = useState(false); + + const barChartData: ChartSeriesData[] = getCustomChartData(data, mapping); + + useEffect(() => { + if (totalCount >= 0 && loadingInitial) { + setLoadingInitial(false); + } + }, [loading]); + + return ( + setShowInspect(true)} + onMouseLeave={() => setShowInspect(false)} + > + + + {loadingInitial ? ( + + ) : ( + <> + + + {loading && ( + + )} + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts new file mode 100644 index 0000000000000..edcd8e3cb9d5c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScaleType } from '@elastic/charts'; +import { MatrixOverTimeHistogramData, MatrixOverOrdinalHistogramData } from '../../graphql/types'; +import { AuthMatrixDataFields } from '../page/hosts/authentications_over_time/utils'; +import { UpdateDateRange } from '../charts/common'; + +export type MatrixHistogramDataTypes = MatrixOverTimeHistogramData | MatrixOverOrdinalHistogramData; +export type MatrixHistogramMappingTypes = AuthMatrixDataFields; +export interface MatrixHistogramBasicProps { + data: T[]; + endDate: number; + id: string; + loading: boolean; + mapping?: MatrixHistogramMappingTypes; + startDate: number; + totalCount: number; + updateDateRange: UpdateDateRange; +} + +export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + dataKey?: string; + scaleType?: ScaleType; + subtitle?: string; + title?: string; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts new file mode 100644 index 0000000000000..1eb5e96b86857 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts'; +import { get, groupBy, map, toPairs } from 'lodash/fp'; +import numeral from '@elastic/numeral'; +import { UpdateDateRange, ChartSeriesData } from '../charts/common'; +import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; + +export const getBarchartConfigs = ({ + from, + to, + scaleType, + onBrushEnd, + yTickFormatter, + showLegend, +}: { + from: number; + to: number; + scaleType: ScaleType; + onBrushEnd: UpdateDateRange; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; +}) => ({ + series: { + xScaleType: scaleType || ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: ['g'], + }, + axis: { + xTickFormatter: scaleType === ScaleType.Time ? niceTimeFormatter([from, to]) : undefined, + yTickFormatter: + yTickFormatter != null + ? yTickFormatter + : (value: string | number): string => value.toLocaleString(), + tickSize: 8, + }, + settings: { + legendPosition: Position.Bottom, + onBrushEnd, + showLegend: showLegend || true, + theme: { + scales: { + barsPadding: 0.08, + }, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + chartPaddings: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + }, + customHeight: 324, +}); + +export const formatToChartDataItem = ([key, value]: [ + string, + MatrixHistogramDataTypes[] +]): ChartSeriesData => ({ + key, + value, +}); + +export const getCustomChartData = ( + data: MatrixHistogramDataTypes[], + mapping?: MatrixHistogramMappingTypes +): ChartSeriesData[] => { + const dataGroupedByEvent = groupBy('g', data); + const dataGroupedEntries = toPairs(dataGroupedByEvent); + const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); + + if (mapping) + return map((item: ChartSeriesData) => { + const customColor = get(`${item.key}.color`, mapping); + item.color = customColor; + return item; + }, formattedChartData); + else return formattedChartData; +}; + +export const bytesFormatter = (value: number) => { + return numeral(value).format('0,0.[0]b'); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx deleted file mode 100644 index 9d2ef203361bf..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx +++ /dev/null @@ -1,84 +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 { shallow } from 'enzyme'; -import * as React from 'react'; - -import { MatrixOverTimeHistogram } from '.'; - -jest.mock('@elastic/eui', () => { - return { - EuiPanel: (children: JSX.Element) => <>{children}, - EuiLoadingContent: () =>
, - }; -}); - -jest.mock('../loader', () => { - return { - Loader: () =>
, - }; -}); - -jest.mock('../../lib/settings/use_kibana_ui_setting', () => { - return { useKibanaUiSetting: () => [false] }; -}); - -jest.mock('../header_section', () => { - return { - HeaderSection: () =>
, - }; -}); - -jest.mock('../charts/barchart', () => { - return { - BarChart: () =>
, - }; -}); - -describe('Load More Events Table Component', () => { - const mockMatrixOverTimeHistogramProps = { - data: [], - dataKey: 'mockDataKey', - endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), - id: 'mockId', - loading: true, - updateDateRange: () => {}, - startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), - subtitle: 'mockSubtitle', - totalCount: -1, - title: 'mockTitle', - }; - describe('rendering', () => { - test('it renders EuiLoadingContent on initialLoad', () => { - const wrapper = shallow(); - - expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy(); - }); - - test('it renders Loader while fetching data if visited before', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, - loading: true, - }; - const wrapper = shallow(); - expect(wrapper.find('.loader')).toBeTruthy(); - }); - - test('it renders BarChart if data available', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, - loading: false, - }; - const wrapper = shallow(); - - expect(wrapper.find(`.barchart`)).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx deleted file mode 100644 index 75e1531ea2b5b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx +++ /dev/null @@ -1,142 +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 React, { useState, useEffect } from 'react'; -import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts'; - -import { getOr, head, last } from 'lodash/fp'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { EuiLoadingContent } from '@elastic/eui'; -import { BarChart } from '../charts/barchart'; -import { HeaderSection } from '../header_section'; -import { ChartSeriesData, UpdateDateRange } from '../charts/common'; -import { MatrixOverTimeHistogramData } from '../../graphql/types'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; -import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; -import { Loader } from '../loader'; -import { Panel } from '../panel'; - -export interface MatrixOverTimeBasicProps { - id: string; - data: MatrixOverTimeHistogramData[]; - loading: boolean; - startDate: number; - endDate: number; - updateDateRange: UpdateDateRange; - totalCount: number; -} - -export interface MatrixOverTimeProps extends MatrixOverTimeBasicProps { - customChartData?: ChartSeriesData[]; - title: string; - subtitle?: string; - dataKey: string; -} - -const getBarchartConfigs = (from: number, to: number, onBrushEnd: UpdateDateRange) => ({ - series: { - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - stackAccessors: ['g'], - }, - axis: { - xTickFormatter: niceTimeFormatter([from, to]), - yTickFormatter: (value: string | number): string => value.toLocaleString(), - tickSize: 8, - }, - settings: { - legendPosition: Position.Bottom, - onBrushEnd, - showLegend: true, - theme: { - scales: { - barsPadding: 0.08, - }, - chartMargins: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - chartPaddings: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - }, - }, - customHeight: 324, -}); - -export const MatrixOverTimeHistogram = ({ - customChartData, - id, - loading, - data, - dataKey, - endDate, - updateDateRange, - startDate, - title, - subtitle, - totalCount, -}: MatrixOverTimeProps) => { - const bucketStartDate = getOr(startDate, 'x', head(data)); - const bucketEndDate = getOr(endDate, 'x', last(data)); - const barchartConfigs = getBarchartConfigs(bucketStartDate!, bucketEndDate!, updateDateRange); - const [showInspect, setShowInspect] = useState(false); - const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE); - const [loadingInitial, setLoadingInitial] = useState(false); - - const barChartData: ChartSeriesData[] = customChartData || [ - { - key: dataKey, - value: data, - }, - ]; - - useEffect(() => { - if (totalCount >= 0 && loadingInitial) { - setLoadingInitial(false); - } - }, [loading]); - - return ( - setShowInspect(true)} - onMouseLeave={() => setShowInspect(false)} - > - - - {loadingInitial ? ( - - ) : ( - <> - - - {loading && ( - - )} - - )} - - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index 8760dce2b76df..9813456e41638 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -82,7 +82,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` } @@ -105,9 +105,9 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` - + 17 - + , "title": "Max Anomaly Score", }, diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx index a764727e8af26..fc76780ef80c7 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { AddNote } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap index 7a8232357d3e1..c9a40975e7b92 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap @@ -1,7 +1,528 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NoteCardBody renders correctly against snapshot 1`] = ` - + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx index 26dc0ca455f73..e5047662eef67 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -7,7 +7,6 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import 'jest-styled-components'; import * as React from 'react'; import { ThemeProvider } from 'styled-components'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx index ad343933a268c..f9e63ee60da5b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx @@ -7,21 +7,23 @@ import React from 'react'; import * as i18n from './translation'; -import { getCustomChartData } from './utils'; -import { MatrixOverTimeHistogram, MatrixOverTimeBasicProps } from '../../../matrix_over_time'; +import { MatrixHistogram } from '../../../matrix_histogram'; +import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; +import { authMatrixDataMappingFields } from './utils'; -export const AuthenticationsOverTimeHistogram = (props: MatrixOverTimeBasicProps) => { +export const AuthenticationsOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { const dataKey = 'authenticationsOverTime'; const { data, ...matrixOverTimeProps } = props; - const customChartData = getCustomChartData(data); - return ( - ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts index 3cc89eeff6540..e0e2d21b40446 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts @@ -4,36 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { groupBy, map, toPairs } from 'lodash/fp'; - import { ChartSeriesData } from '../../../charts/common'; -import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; import { KpiHostsChartColors } from '../kpi_hosts/types'; -const formatToChartDataItem = ([key, value]: [ - string, - MatrixOverTimeHistogramData[] -]): ChartSeriesData => ({ - key, - value, -}); - -const addCustomColors = (item: ChartSeriesData) => { - if (item.key === 'authentication_success') { - item.color = KpiHostsChartColors.authSuccess; - } - - if (item.key === 'authentication_failure') { - item.color = KpiHostsChartColors.authFailure; - } - - return item; -}; - -export const getCustomChartData = (data: MatrixOverTimeHistogramData[]): ChartSeriesData[] => { - const dataGroupedByEvent = groupBy('g', data); - const dataGroupedEntries = toPairs(dataGroupedByEvent); - const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); - - return map(addCustomColors, formattedChartData); +enum AuthMatrixDataGroup { + authSuccess = 'authentication_success', + authFailure = 'authentication_failure', +} + +export interface AuthMatrixDataFields { + [AuthMatrixDataGroup.authSuccess]: ChartSeriesData; + [AuthMatrixDataGroup.authFailure]: ChartSeriesData; +} + +export const authMatrixDataMappingFields: AuthMatrixDataFields = { + [AuthMatrixDataGroup.authSuccess]: { + key: AuthMatrixDataGroup.authSuccess, + value: null, + color: KpiHostsChartColors.authSuccess, + }, + [AuthMatrixDataGroup.authFailure]: { + key: AuthMatrixDataGroup.authFailure, + value: null, + color: KpiHostsChartColors.authFailure, + }, }; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx index 7dd5eccb4a6c6..71e61e2425373 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx @@ -49,7 +49,7 @@ describe('Authentication Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(AuthenticationTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx index 8b41619199653..8273ecffdf9b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx @@ -7,16 +7,20 @@ import React from 'react'; import * as i18n from './translation'; -import { MatrixOverTimeHistogram, MatrixOverTimeBasicProps } from '../../../matrix_over_time'; +import { MatrixHistogram } from '../../../matrix_histogram'; +import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; -export const EventsOverTimeHistogram = (props: MatrixOverTimeBasicProps) => { +export const EventsOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { const dataKey = 'eventsOverTime'; const { totalCount } = props; const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`; const { ...matrixOverTimeProps } = props; return ( - +) => { + const dataKey = 'histogram'; + const { ...matrixOverTimeProps } = props; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts new file mode 100644 index 0000000000000..bb822651f10ce --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NETWORK_DNS_HISTOGRAM = i18n.translate('xpack.siem.DNS.histogramTitle', { + defaultMessage: 'Top DNS domains bytes count', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx index eb6204044bdb7..964617c4c85b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx @@ -42,7 +42,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); test('it renders the default widget', () => { @@ -59,7 +59,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index 98f55b29c8fc4..8bf338d17c47b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkDnsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts index 29ea5f9d12588..281125edb9dc4 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts @@ -126,5 +126,57 @@ export const mockData: { NetworkDns: NetworkDnsData } = { fakeTotalCount: 50, showMorePagesIndicator: true, }, + histogram: [ + { + x: 'nflxvideo.net', + g: 'nflxvideo.net', + y: 12546, + }, + { + x: 'apple.com', + g: 'apple.com', + y: 31687, + }, + { + x: 'googlevideo.com', + g: 'googlevideo.com', + y: 16292, + }, + { + x: 'netflix.com', + g: 'netflix.com', + y: 218193, + }, + { + x: 'samsungcloudsolution.com', + g: 'samsungcloudsolution.com', + y: 11702, + }, + { + x: 'doubleclick.net', + g: 'doubleclick.net', + y: 14372, + }, + { + x: 'digitalocean.com', + g: 'digitalocean.com', + y: 4111, + }, + { + x: 'samsungelectronics.com', + g: 'samsungelectronics.com', + y: 36592, + }, + { + x: 'google.com', + g: 'google.com', + y: 8072, + }, + { + x: 'samsungcloudsolution.net', + g: 'samsungcloudsolution.net', + y: 11518, + }, + ], }, }; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index 277e136d776fa..c92661a909a6e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkHttp Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkHttpTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index d8a5da6036f95..ca7a3c0bb4387 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -82,7 +82,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index df9e0f9f89645..884825422beb0 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); test('it renders the default NetworkTopNFlow table on the IP Details page', () => { @@ -83,7 +83,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 612896c878ef9..8c397053380c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -47,7 +47,7 @@ describe('Tls Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(TlsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index 00a0a34a2b30b..d178164fd3fd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -55,7 +55,7 @@ describe('Users Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(UsersTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index cee2c18710e74..8f592c7bbba60 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -73,7 +73,7 @@ const overviewNetworkStats = (data: OverviewNetworkData) => [ title: ( ), id: 'filebeatPanw', diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap index 14c5538843ef5..4ac25720080a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -1,107 +1,628 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Paginated Table Component rendering it renders the default load more table 1`] = ` - - My test supplement. -

- } - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={ - Array [ - Object { - "numberOfRow": 2, - "text": "2 rows", - }, - Object { - "numberOfRow": 5, - "text": "5 rows", - }, - Object { - "numberOfRow": 10, - "text": "10 rows", - }, - Object { - "numberOfRow": 20, - "text": "20 rows", - }, - Object { - "numberOfRow": 50, - "text": "50 rows", - }, - ] - } - limit={1} - loadPage={[Function]} - loading={false} - pageOfItems={ - Array [ - Object { - "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", - }, - "host": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", - "firstSeen": "2018-12-06T15:40:53.319Z", - "name": "elrond.elstc.co", - "os": "Ubuntu", - "version": "18.04.1 LTS (Bionic Beaver)", + +> + + My test supplement. +

+ } + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={ + Array [ + Object { + "numberOfRow": 2, + "text": "2 rows", + }, + Object { + "numberOfRow": 5, + "text": "5 rows", + }, + Object { + "numberOfRow": 10, + "text": "10 rows", + }, + Object { + "numberOfRow": 20, + "text": "20 rows", + }, + Object { + "numberOfRow": 50, + "text": "50 rows", + }, + ] + } + limit={1} + loadPage={[Function]} + loading={false} + pageOfItems={ + Array [ + Object { + "cursor": Object { + "value": "98966fa2013c396155c460d35c0902be", + }, + "host": Object { + "_id": "cPsuhGcB0WOhS6qyTKC0", + "firstSeen": "2018-12-06T15:40:53.319Z", + "name": "elrond.elstc.co", + "os": "Ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)", + }, + }, + Object { + "cursor": Object { + "value": "aa7ca589f1b8220002f2fc61c64cfbf1", + }, + "host": Object { + "_id": "KwQDiWcB0WOhS6qyXmrW", + "firstSeen": "2018-12-07T14:12:38.560Z", + "name": "siem-kibana", + "os": "Debian GNU/Linux", + "version": "9 (stretch)", + }, + }, + ] + } + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={[Function]} + updateLimitPagination={[Function]} + /> +
`; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index 7be0c1885811b..aedec1a340bfd 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { memo, useState, useEffect } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { Direction } from '../../graphql/types'; import { AuthTableColumns } from '../page/hosts/authentications_table'; @@ -319,37 +319,35 @@ const BasicTable = styled(EuiBasicTable)` BasicTable.displayName = 'BasicTable'; -const FooterAction = styled(EuiFlexGroup).attrs({ +const FooterAction = styled(EuiFlexGroup).attrs(() => ({ alignItems: 'center', responsive: false, -})` - margin-top: ${props => props.theme.eui.euiSizeXS}; +}))` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; `; FooterAction.displayName = 'FooterAction'; const PaginationEuiFlexItem = styled(EuiFlexItem)` - ${props => css` - @media only screen and (min-width: ${props.theme.eui.euiBreakpoints.m}) { - .euiButtonIcon:last-child { - margin-left: 28px; - } + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + .euiButtonIcon:last-child { + margin-left: 28px; + } - .euiPagination { - position: relative; - } + .euiPagination { + position: relative; + } - .euiPagination::before { - bottom: 0; - color: ${props.theme.eui.euiButtonColorDisabled}; - content: '\\2026'; - font-size: ${props.theme.eui.euiFontSizeS}; - padding: 5px ${props.theme.eui.euiSizeS}; - position: absolute; - right: ${props.theme.eui.euiSizeL}; - } + .euiPagination::before { + bottom: 0; + color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; + content: '\\2026'; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + padding: 5px ${({ theme }) => theme.eui.euiSizeS}; + position: absolute; + right: ${({ theme }) => theme.eui.euiSizeL}; } - `} + } `; PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index d619b515ccc7a..ce102d7ade53b 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -84,6 +84,7 @@ describe('QueryBar ', () => { } = wrapper.find(SearchBar).props(); expect(searchBarProps).toEqual({ + dataTestSubj: undefined, dateRangeFrom: 'now-24h', dateRangeTo: 'now', filters: [], @@ -178,6 +179,7 @@ describe('QueryBar ', () => { title: 'filebeat-*,auditbeat-*,packetbeat-*', }, ], + isLoading: false, isRefreshPaused: true, query: { language: 'kuery', diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index c7e58532fc7e5..3f460560b79b5 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -20,10 +20,12 @@ import { SavedQueryTimeFilter } from '../../../../../../../src/legacy/core_plugi import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; export interface QueryBarComponentProps { + dataTestSubj?: string; dateRangeFrom?: string; dateRangeTo?: string; hideSavedQuery?: boolean; indexPattern: StaticIndexPattern; + isLoading?: boolean; isRefreshPaused?: boolean; filterQuery: Query; filterManager: FilterManager; @@ -41,6 +43,7 @@ export const QueryBar = memo( dateRangeTo, hideSavedQuery = false, indexPattern, + isLoading = false, isRefreshPaused, filterQuery, filterManager, @@ -50,6 +53,7 @@ export const QueryBar = memo( refreshInterval, savedQuery, onSavedQuery, + dataTestSubj, }) => { const [draftQuery, setDraftQuery] = useState(filterQuery); @@ -123,6 +127,7 @@ export const QueryBar = memo( dateRangeTo={dateRangeTo} filters={filters} indexPatterns={indexPatterns} + isLoading={isLoading} isRefreshPaused={isRefreshPaused} query={draftQuery} onClearSavedQuery={onClearSavedQuery} @@ -139,6 +144,7 @@ export const QueryBar = memo( showQueryInput={true} showSaveQuery={true} timeHistory={new TimeHistory(new Storage(localStorage))} + dataTestSubj={dataTestSubj} {...searchBarProps} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap index 432abd799b48a..6ae630f363b58 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/resize_handle/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Resizeable it renders 1`] = ` } diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx index 6bb036a88a9d7..f84276f99315d 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { TestProviders } from '../../mock/test_providers'; diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx index 0a6203056fd20..eb3326c2f2cd0 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef } from 'react'; import { fromEvent, Observable, Subscription } from 'rxjs'; import { concatMap, takeUntil } from 'rxjs/operators'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export type OnResize = ({ delta, id }: { delta: number; id: string }) => void; @@ -42,16 +42,14 @@ interface Props extends ResizeHandleContainerProps { } const ResizeHandleContainer = styled.div` - ${({ bottom, height, left, positionAbsolute, right, theme, top }) => css` - bottom: ${positionAbsolute && bottom}; - cursor: ${resizeCursorStyle}; - height: ${height}; - left: ${positionAbsolute && left}; - position: ${positionAbsolute && 'absolute'}; - right: ${positionAbsolute && right}; - top: ${positionAbsolute && top}; - z-index: ${positionAbsolute && theme.eui.euiZLevel1}; - `} + bottom: ${({ positionAbsolute, bottom }) => positionAbsolute && bottom}; + cursor: ${resizeCursorStyle}; + height: ${({ height }) => height}; + left: ${({ positionAbsolute, left }) => positionAbsolute && left}; + position: ${({ positionAbsolute }) => positionAbsolute && 'absolute'}; + right: ${({ positionAbsolute, right }) => positionAbsolute && right}; + top: ${({ positionAbsolute, top }) => positionAbsolute && top}; + z-index: ${({ positionAbsolute, theme }) => positionAbsolute && theme.eui.euiZLevel1}; `; ResizeHandleContainer.displayName = 'ResizeHandleContainer'; @@ -69,7 +67,7 @@ export const Resizeable = React.memo( const dragEventTargets = useRef>([]); const dragSubscription = useRef(null); const prevX = useRef(0); - const ref = useRef>(React.createRef()); + const ref = useRef(null); const upSubscription = useRef(null); const isResizingRef = useRef(false); @@ -80,7 +78,7 @@ export const Resizeable = React.memo( }; useEffect(() => { const move$ = fromEvent(document, 'mousemove'); - const down$ = fromEvent(ref.current.current!, 'mousedown'); + const down$ = fromEvent(ref.current!, 'mousedown'); const up$ = fromEvent(document, 'mouseup'); drag$.current = down$.pipe(concatMap(() => move$.pipe(takeUntil(up$)))); @@ -131,7 +129,7 @@ export const Resizeable = React.memo( bottom={bottom} data-test-subj="resize-handle-container" height={height} - innerRef={ref.current} + ref={ref} left={left} positionAbsolute={positionAbsolute} right={right} diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx index 1bd30bad818d1..988bb13841fa5 100644 --- a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx @@ -23,10 +23,7 @@ describe('Scroll to top', () => { Object.defineProperty(globalNode.window, 'scroll', { value: spyScroll }); mount( useScrollToTop()} />); - expect(spyScroll).toHaveBeenCalledWith({ - top: 0, - left: 0, - }); + expect(spyScroll).toHaveBeenCalledWith(0, 0); }); test('scrollTo have been called', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx index 59f9c99d6ab8e..8d4548516fc16 100644 --- a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx @@ -10,10 +10,7 @@ export const useScrollToTop = () => { useEffect(() => { // trying to use new API - https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo if (window.scroll) { - window.scroll({ - top: 0, - left: 0, - }); + window.scroll(0, 0); } else { // just a fallback for older browsers window.scrollTo(0, 0); diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index c16c3a33872d5..33fb2b9239a6a 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -71,6 +71,7 @@ interface SiemSearchBarProps { id: InputsModelId; indexPattern: StaticIndexPattern; timelineId?: string; + dataTestSubj?: string; } const SearchBarContainer = styled.div` @@ -95,6 +96,7 @@ const SearchBarComponent = memo { const { timefilter } = npStart.plugins.data.query.timefilter; if (fromStr != null && toStr != null) { @@ -275,6 +277,7 @@ const SearchBarComponent = memo ); diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx index 3ecbfcc630752..95c68d0233c69 100644 --- a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { SelectableText } from '.'; diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx index b5147d395c738..fc1c6e00edc03 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import React from 'react'; import { TestProviders } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx index 6359063798ba8..20aea3251d838 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx @@ -5,24 +5,22 @@ */ import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; interface RowProps { rowHeight?: string; rowPadding?: string; } -const Row = styled.div.attrs({ +const Row = styled.div.attrs(({ rowHeight, rowPadding, theme }) => ({ className: 'siemSkeletonRow', -})` - ${({ rowHeight, rowPadding, theme }) => css` - border-bottom: ${theme.eui.euiBorderThin}; - display: flex; - height: ${rowHeight ? rowHeight : theme.eui.euiSizeXL}; - padding: ${rowPadding - ? rowPadding - : theme.eui.paddingSizes.s + ' ' + theme.eui.paddingSizes.xs}; - `} + rowHeight: rowHeight || theme.eui.euiSizeXL, + rowPadding: rowPadding || `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.xs}`, +}))` + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + display: flex; + height: ${({ rowHeight }) => rowHeight}; + padding: ${({ rowPadding }) => rowPadding}; `; Row.displayName = 'Row'; @@ -31,18 +29,18 @@ interface CellProps { cellMargin?: string; } -const Cell = styled.div.attrs({ +const Cell = styled.div.attrs(({ cellColor, cellMargin, theme }) => ({ className: 'siemSkeletonRow__cell', -})` - ${({ cellColor, cellMargin, theme }) => css` - background-color: ${cellColor ? cellColor : theme.eui.euiColorLightestShade}; - border-radius: 2px; - flex: 1; + cellColor: cellColor || theme.eui.euiColorLightestShade, + cellMargin: cellMargin || theme.eui.gutterTypes.gutterSmall, +}))` + background-color: ${({ cellColor }) => cellColor}; + border-radius: 2px; + flex: 1; - & + & { - margin-left: ${cellMargin ? cellMargin : theme.eui.gutterTypes.gutterSmall}; - } - `} + & + & { + margin-left: ${({ cellMargin }) => cellMargin}; + } `; Cell.displayName = 'Cell'; diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 7475220b56e77..952d92f532c83 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -38,11 +38,11 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = data-test-subj="stat-item" >

@@ -292,11 +292,11 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] = data-test-subj="stat-item" >

@@ -616,11 +616,11 @@ exports[`Stat Items Component rendering kpis with charts it renders the default data-test-subj="stat-item" >

1,714 @@ -857,10 +857,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default key="stat-items-field-uniqueDestinationIps" >

2,359 @@ -957,10 +957,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default >

(({ children }) => { - if (typeof children === 'string') { - return

{children}

; - } else { - return
{children}
; +const SubtitleItem = React.memo( + ({ children, dataTestSubj = 'header-panel-subtitle' }) => { + if (typeof children === 'string') { + return ( +

+ {children} +

+ ); + } else { + return ( +
+ {children} +
+ ); + } } -}); +); SubtitleItem.displayName = 'SubtitleItem'; export interface SubtitleProps { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 8bf7b1543b923..65818b697e0b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -483,8 +483,12 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index c605e9e4bfe62..4f414af74a914 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -8,17 +8,16 @@ import { EuiCheckbox, EuiSuperSelect } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; import { pure } from 'recompose'; -import styled, { injectGlobal } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; import { getEventsSelectOptions } from './helpers'; export type CheckState = 'checked' | 'indeterminate' | 'unchecked'; export const EVENTS_SELECT_WIDTH = 60; // px -// SIDE EFFECT: the following `injectGlobal` overrides +// SIDE EFFECT: the following `createGlobalStyle` overrides // the style of the select items -// eslint-disable-next-line -injectGlobal` +const EventsSelectGlobalStyle = createGlobalStyle` .eventsSelectItem { width: 100% !important; @@ -73,6 +72,7 @@ export const EventsSelect = pure(({ checkState, timelineId }) => { /> +
); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap index b009ec78aac59..64c2b6ed10692 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Header renders correctly against snapshot 1`] = ` } + handle={} id="@timestamp" onResize={[Function]} positionAbsolute={true} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index 69899ece0cea8..ce465ac4f837e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -6,7 +6,6 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; -import 'jest-styled-components'; import * as React from 'react'; import { Direction } from '../../../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index c4239c0eb7f38..aca74b3da645f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -115,7 +115,7 @@ export const ColumnHeaders = React.memo( {dropProvided => ( {columnHeaders.map((header, i) => ( @@ -137,7 +137,7 @@ export const ColumnHeaders = React.memo( {...dragProvided.draggableProps} {...dragProvided.dragHandleProps} data-test-subj="draggable-header" - innerRef={dragProvided.innerRef} + ref={dragProvided.innerRef} isDragging={dragSnapshot.isDragging} position="relative" // Passing the styles directly to the component because the width is being calculated and is recommended by Styled Components for performance: https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index d54fe8df28a85..b3ef4b7b39466 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -200,7 +200,7 @@ export const StatefulEvent = React.memo( { + ref={c => { if (c != null) { divElement.current = c; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index 86a6ebe22799b..0fb4c4f375684 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -5,7 +5,6 @@ */ import { mount, ReactWrapper } from 'enzyme'; -import 'jest-styled-components'; import * as React from 'react'; import { mockBrowserFields } from '../../../containers/source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 47fbcec4aab23..07e37346ac968 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -19,7 +19,7 @@ import { OnUnPinEvent, OnUpdateColumns, } from '../events'; -import { EventsTable, TimelineBody } from '../styles'; +import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { ColumnHeader } from './column_headers/column_header'; import { Events } from './events'; @@ -86,50 +86,53 @@ export const Body = React.memo( ); return ( - - - + <> + + + - - - + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 29d2df5172457..3ef7240ee0375 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -50,7 +50,7 @@ const HighlightedBackground = styled.span` HighlightedBackground.displayName = 'HighlightedBackground'; const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` - width: ${props => (props.showSmallMsg ? '60px' : 'auto')} + width: ${props => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; flex-direction: row; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx index b7d0f2ff50bc1..59d15bc43e10c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -53,7 +53,7 @@ interface ProviderBadgeProps { isEnabled: boolean; isExcluded: boolean; providerId: string; - togglePopover?: () => void; + togglePopover: () => void; val: string | number; operator: QueryOperator; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx index ee9e5f2af654a..121f832221d3e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx @@ -3,8 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui'; -import React from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiPopover, + EuiPopoverProps, +} from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../containers/source'; @@ -41,7 +46,11 @@ interface OwnProps { value: string | number; } -const MyEuiPopover = styled(EuiPopover)` +const MyEuiPopover = styled((EuiPopover as unknown) as FunctionComponent)< + EuiPopoverProps & { + id?: string; + } +>` height: 100%; user-select: none; `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 112962367cd36..5a8654509fa88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -46,7 +46,7 @@ interface Props { const ROW_OF_DATA_PROVIDERS_HEIGHT = 43; // px const PanelProviders = styled.div` - position: relative + position: relative; display: flex; flex-direction: row; min-height: 100px; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index a0942cbaba091..93f1e484828d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -16,9 +16,10 @@ import { EuiPopover, EuiText, EuiToolTip, + EuiPopoverProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; @@ -55,7 +56,12 @@ const LoadingPanelContainer = styled.div` LoadingPanelContainer.displayName = 'LoadingPanelContainer'; -const PopoverRowItems = styled(EuiPopover)` +const PopoverRowItems = styled((EuiPopover as unknown) as FunctionComponent)< + EuiPopoverProps & { + className?: string; + id?: string; + } +>` .euiButtonEmpty__content { padding: 0px 0px; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index 40ba16c0c128a..7b69e006f48ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import styled, { injectGlobal } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -22,43 +20,6 @@ type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; -// SIDE EFFECT: the following `injectGlobal` overrides `EuiPopover` -// and `EuiToolTip` global styles: -// eslint-disable-next-line no-unused-expressions -injectGlobal` - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - } - .euiToolTip { - z-index: 9950 !important; - } -`; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - interface Props { associateNote: AssociateNote; createTimeline: CreateTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index 6d76c277711d7..f24ee3155c924 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -288,6 +288,7 @@ export const QueryBarTimeline = memo( refreshInterval={refreshInterval} savedQuery={savedQuery} onSavedQuery={onSavedQuery} + dataTestSubj={'timelineQueryInput'} /> ); } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index db8909adda239..eaa476bf3e2b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; import { pure } from 'recompose'; -import styled, { injectGlobal } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -26,8 +26,7 @@ const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; const searchOrFilterPopoverWidth = '352px'; // SIDE EFFECT: the following creates a global class selector -// eslint-disable-next-line no-unused-expressions -injectGlobal` +const SearchOrFilterGlobalStyle = createGlobalStyle` .${timelineSelectModeItemsClassName} { width: 350px !important; } @@ -110,48 +109,51 @@ export const SearchOrFilter = pure( updateKqlMode, updateReduxTime, }) => ( - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + <> + + + + + updateKqlMode({ id: timelineId, kqlMode: mode })} + options={options} + popoverClassName={searchOrFilterPopoverClassName} + valueOfSelected={kqlMode} + /> + + + + - - - - - - - +
+ + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index 86c470ef4d3a5..1c1c8fac75cdc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -6,7 +6,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; -import styled, { css } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; + +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; /** * OFFSET PIXEL VALUES @@ -18,30 +20,35 @@ export const OFFSET_SCROLLBAR = 17; * TIMELINE BODY */ -export const TimelineBody = styled.div.attrs({ - className: 'siemTimeline__body', -})<{ bodyHeight: number }>` - ${({ bodyHeight, theme }) => css` - height: ${bodyHeight + 'px'}; - overflow: auto; - scrollbar-width: thin; - - &::-webkit-scrollbar { - height: ${theme.eui.euiScrollBar}; - width: ${theme.eui.euiScrollBar}; - } +// SIDE EFFECT: the following creates a global class selector +export const TimelineBodyGlobalStyle = createGlobalStyle` + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .siemTimeline__body { + overflow: hidden; + } +`; - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${theme.eui.euiScrollBarCorner} solid transparent; - } +export const TimelineBody = styled.div.attrs(({ className }) => ({ + className: `siemTimeline__body ${className}`, +}))<{ bodyHeight: number }>` + height: ${({ bodyHeight }) => bodyHeight + 'px'}; + overflow: auto; + scrollbar-width: thin; - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } - `} + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } `; TimelineBody.displayName = 'TimelineBody'; @@ -49,39 +56,38 @@ TimelineBody.displayName = 'TimelineBody'; * EVENTS TABLE */ -export const EventsTable = styled.div.attrs({ - className: 'siemEventsTable', +export const EventsTable = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable ${className}`, role: 'table', -})``; +}))``; EventsTable.displayName = 'EventsTable'; /* EVENTS HEAD */ -export const EventsThead = styled.div.attrs({ - className: 'siemEventsTable__thead', +export const EventsThead = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__thead ${className}`, role: 'rowgroup', -})` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorEmptyShade}; - border-bottom: ${theme.eui.euiBorderWidthThick} solid ${theme.eui.euiColorLightShade}; - position: sticky; - top: 0; - z-index: ${theme.eui.euiZLevel1}; - `} +}))` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThick} solid + ${({ theme }) => theme.eui.euiColorLightShade}; + position: sticky; + top: 0; + z-index: ${({ theme }) => theme.eui.euiZLevel1}; `; EventsThead.displayName = 'EventsThead'; -export const EventsTrHeader = styled.div.attrs({ - className: 'siemEventsTable__trHeader', +export const EventsTrHeader = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__trHeader ${className}`, role: 'row', -})` +}))` display: flex; `; EventsTrHeader.displayName = 'EventsTrHeader'; -export const EventsThGroupActions = styled.div.attrs({ - className: 'siemEventsTable__thGroupActions', -})<{ actionsColumnWidth: number }>` +export const EventsThGroupActions = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__thGroupActions ${className}`, +}))<{ actionsColumnWidth: number }>` display: flex; flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; justify-content: space-between; @@ -89,17 +95,17 @@ export const EventsThGroupActions = styled.div.attrs({ `; EventsThGroupActions.displayName = 'EventsThGroupActions'; -export const EventsThGroupData = styled.div.attrs({ - className: 'siemEventsTable__thGroupData', -})` +export const EventsThGroupData = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__thGroupData ${className}`, +}))` display: flex; `; EventsThGroupData.displayName = 'EventsThGroupData'; -export const EventsTh = styled.div.attrs({ - className: 'siemEventsTable__th', +export const EventsTh = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__th ${className}`, role: 'columnheader', -})<{ isDragging?: boolean; position?: string }>` +}))<{ isDragging?: boolean; position?: string }>` align-items: center; display: flex; flex-shrink: 0; @@ -118,66 +124,62 @@ export const EventsTh = styled.div.attrs({ `; EventsTh.displayName = 'EventsTh'; -export const EventsThContent = styled.div.attrs({ - className: 'siemEventsTable__thContent', -})<{ textAlign?: string }>` - ${({ textAlign, theme }) => css` - font-size: ${theme.eui.euiFontSizeXS}; - font-weight: ${theme.eui.euiFontWeightSemiBold}; - line-height: ${theme.eui.euiLineHeight}; - min-width: 0; - padding: ${theme.eui.paddingSizes.xs}; - text-align: ${textAlign}; - width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 - `} +export const EventsThContent = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__thContent ${className}`, +}))<{ textAlign?: string }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + line-height: $({ theme }) =>theme.eui.euiLineHeight}; + min-width: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; + text-align: ${({ textAlign }) => textAlign}; + width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 `; EventsThContent.displayName = 'EventsThContent'; /* EVENTS BODY */ -export const EventsTbody = styled.div.attrs({ - className: 'siemEventsTable__tbody', +export const EventsTbody = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tbody ${className}`, role: 'rowgroup', -})` +}))` overflow-x: hidden; `; EventsTbody.displayName = 'EventsTbody'; -export const EventsTrGroup = styled.div.attrs({ - className: 'siemEventsTable__trGroup', -})<{ className?: string }>` - ${({ theme }) => css` - border-bottom: ${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiColorLightShade}; +export const EventsTrGroup = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__trGroup ${className}`, +}))<{ className?: string }>` + border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid + ${({ theme }) => theme.eui.euiColorLightShade}; - &:hover { - background-color: ${theme.eui.euiTableHoverColor}; - } - `} + &:hover { + background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; + } `; EventsTrGroup.displayName = 'EventsTrGroup'; -export const EventsTrData = styled.div.attrs({ - className: 'siemEventsTable__trData', +export const EventsTrData = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__trData ${className}`, role: 'row', -})` +}))` display: flex; `; EventsTrData.displayName = 'EventsTrData'; -export const EventsTrSupplement = styled.div.attrs({ - className: 'siemEventsTable__trSupplement', -})<{ className: string }>` - ${({ theme }) => css` - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - padding: 0 ${theme.eui.paddingSizes.xs} 0 ${theme.eui.paddingSizes.xl}; - `} +export const EventsTrSupplement = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__trSupplement ${className}`, +}))<{ className: string }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 + ${({ theme }) => theme.eui.paddingSizes.xl}; `; EventsTrSupplement.displayName = 'EventsTrSupplement'; -export const EventsTdGroupActions = styled.div.attrs({ - className: 'siemEventsTable__tdGroupActions', -})<{ actionsColumnWidth: number }>` +export const EventsTdGroupActions = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tdGroupActions ${className}`, +}))<{ actionsColumnWidth: number }>` display: flex; justify-content: space-between; flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; @@ -185,17 +187,17 @@ export const EventsTdGroupActions = styled.div.attrs({ `; EventsTdGroupActions.displayName = 'EventsTdGroupActions'; -export const EventsTdGroupData = styled.div.attrs({ - className: 'siemEventsTable__tdGroupData', -})` +export const EventsTdGroupData = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tdGroupData ${className}`, +}))` display: flex; `; EventsTdGroupData.displayName = 'EventsTdGroupData'; -export const EventsTd = styled.div.attrs({ - className: 'siemEventsTable__td', +export const EventsTd = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__td ${className}`, role: 'cell', -})` +}))` align-items: center; display: flex; flex-shrink: 0; @@ -207,17 +209,15 @@ export const EventsTd = styled.div.attrs({ `; EventsTd.displayName = 'EventsTd'; -export const EventsTdContent = styled.div.attrs({ - className: 'siemEventsTable__tdContent', -})<{ textAlign?: string }>` - ${({ textAlign, theme }) => css` - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - min-width: 0; - padding: ${theme.eui.paddingSizes.xs}; - text-align: ${textAlign}; - width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 - `} +export const EventsTdContent = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tdContent ${className}`, +}))<{ textAlign?: string }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + min-width: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; + text-align: ${({ textAlign }) => textAlign}; + width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 `; EventsTdContent.displayName = 'EventsTdContent'; @@ -225,9 +225,9 @@ EventsTdContent.displayName = 'EventsTdContent'; * EVENTS HEADING */ -export const EventsHeading = styled.div.attrs({ - className: 'siemEventsHeading', -})<{ isLoading: boolean }>` +export const EventsHeading = styled.div.attrs(({ className }) => ({ + className: `siemEventsHeading ${className}`, +}))<{ isLoading: boolean }>` align-items: center; display: flex; @@ -237,81 +237,75 @@ export const EventsHeading = styled.div.attrs({ `; EventsHeading.displayName = 'EventsHeading'; -export const EventsHeadingTitleButton = styled.button.attrs({ - className: 'siemEventsHeading__title siemEventsHeading__title--aggregatable', +export const EventsHeadingTitleButton = styled.button.attrs(({ className }) => ({ + className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`, type: 'button', -})` - ${({ theme }) => css` - align-items: center; - display: flex; - font-weight: inherit; - min-width: 0; - - &:hover, - &:focus { - color: ${theme.eui.euiColorPrimary}; - text-decoration: underline; - } +}))` + align-items: center; + display: flex; + font-weight: inherit; + min-width: 0; - &:hover { - cursor: pointer; - } + &:hover, + &:focus { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + text-decoration: underline; + } - & > * + * { - margin-left: ${theme.eui.euiSizeXS}; - } - `} + &:hover { + cursor: pointer; + } + + & > * + * { + margin-left: ${({ theme }) => theme.eui.euiSizeXS}; + } `; EventsHeadingTitleButton.displayName = 'EventsHeadingTitleButton'; -export const EventsHeadingTitleSpan = styled.span.attrs({ - className: 'siemEventsHeading__title siemEventsHeading__title--notAggregatable', -})` +export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({ + className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`, +}))` min-width: 0; `; EventsHeadingTitleSpan.displayName = 'EventsHeadingTitleSpan'; -export const EventsHeadingExtra = styled.div.attrs({ - className: 'siemEventsHeading__extra', -})<{ className?: string }>` - ${({ theme }) => css` - margin-left: auto; - - &.siemEventsHeading__extra--close { - opacity: 0; - transition: all ${theme.eui.euiAnimSpeedNormal} ease; - visibility: hidden; - - .siemEventsTable__th:hover & { - opacity: 1; - visibility: visible; - } - } - `} -`; -EventsHeadingExtra.displayName = 'EventsHeadingExtra'; +export const EventsHeadingExtra = styled.div.attrs(({ className }) => ({ + className: `siemEventsHeading__extra ${className}`, +}))` + margin-left: auto; -export const EventsHeadingHandle = styled.div.attrs({ - className: 'siemEventsHeading__handle', -})` - ${({ theme }) => css` - background-color: ${theme.eui.euiBorderColor}; - height: 100%; + &.siemEventsHeading__extra--close { opacity: 0; - transition: all ${theme.eui.euiAnimSpeedNormal} ease; + transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; visibility: hidden; - width: ${theme.eui.euiBorderWidthThick}; - .siemEventsTable__thead:hover & { + .siemEventsTable__th:hover & { opacity: 1; visibility: visible; } + } +`; +EventsHeadingExtra.displayName = 'EventsHeadingExtra'; - &:hover { - background-color: ${theme.eui.euiColorPrimary}; - cursor: col-resize; - } - `} +export const EventsHeadingHandle = styled.div.attrs(({ className }) => ({ + className: `siemEventsHeading__handle ${className}`, +}))` + background-color: ${({ theme }) => theme.eui.euiBorderColor}; + height: 100%; + opacity: 0; + transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; + visibility: hidden; + width: ${({ theme }) => theme.eui.euiBorderWidthThick}; + + .siemEventsTable__thead:hover & { + opacity: 1; + visibility: visible; + } + + &:hover { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + cursor: col-resize; + } `; EventsHeadingHandle.displayName = 'EventsHeadingHandle'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 59469f96d0720..089fb72ff4c85 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -137,7 +137,7 @@ export const Timeline = React.memo( gutterSize="none" justifyContent="flexStart" > - + props.theme.eui.textColors.default} + color: ${({ theme }) => theme.eui.textColors.default}; height: 100%; position: relative; `; diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 5998aa527206e..309693427459e 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; +import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` ${({ theme }) => css` @@ -54,6 +55,7 @@ export const WrapperPage = React.memo( return ( {children} + ); } diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts new file mode 100644 index 0000000000000..498cdaec131e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const AnomaliesOverTimeGqlQuery = gql` + query GetAnomaliesOverTimeQuery( + $sourceId: ID! + $timerange: TimerangeInput! + $defaultIndex: [String!]! + $filterQuery: String + $inspect: Boolean! + ) { + source(id: $sourceId) { + id + AnomaliesOverTime( + timerange: $timerange + filterQuery: $filterQuery + defaultIndex: $defaultIndex + ) { + anomaliesOverTime { + x + y + g + } + totalCount + inspect @include(if: $inspect) { + dsl + response + } + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx new file mode 100644 index 0000000000000..0d1ffba1ecd82 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; + +import { State, inputsSelectors } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplate } from '../../query_template'; + +import { AnomaliesOverTimeGqlQuery } from './anomalies_over_time.gql_query'; +import { GetAnomaliesOverTimeQuery } from '../../../graphql/types'; +import { AnomaliesOverTimeProps, OwnProps } from './types'; + +const ID = 'anomaliesOverTimeQuery'; + +class AnomaliesOverTimeComponentQuery extends QueryTemplate< + AnomaliesOverTimeProps, + GetAnomaliesOverTimeQuery.Query, + GetAnomaliesOverTimeQuery.Variables +> { + public render() { + const { + children, + endDate, + filterQuery, + id = ID, + isInspected, + sourceId, + startDate, + } = this.props; + + return ( + + query={AnomaliesOverTimeGqlQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + filterQuery, + sourceId, + timerange: { + interval: 'day', + from: startDate!, + to: endDate!, + }, + defaultIndex: ['.ml-anomalies-*'], + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const source = getOr({}, `source.AnomaliesOverTime`, data); + const anomaliesOverTime = getOr([], `anomaliesOverTime`, source); + const totalCount = getOr(-1, 'totalCount', source); + return children!({ + endDate: endDate!, + anomaliesOverTime, + id, + inspect: getOr(null, 'inspect', source), + loading, + refetch, + startDate: startDate!, + totalCount, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AnomaliesOverTimeQuery = connect(makeMapStateToProps)(AnomaliesOverTimeComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts new file mode 100644 index 0000000000000..e6ece4a46e44f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { QueryTemplateProps } from '../../query_template'; +import { inputsModel, hostsModel, networkModel } from '../../../store'; +import { MatrixOverTimeHistogramData } from '../../../graphql/types'; + +export interface AnomaliesArgs { + endDate: number; + anomaliesOverTime: MatrixOverTimeHistogramData[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends Omit { + filterQuery?: string; + children?: (args: AnomaliesArgs) => React.ReactNode; + type: hostsModel.HostsType | networkModel.NetworkType; +} + +export interface AnomaliesOverTimeComponentReduxProps { + isInspected: boolean; +} + +export type AnomaliesOverTimeProps = OwnProps & AnomaliesOverTimeComponentReduxProps; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx new file mode 100644 index 0000000000000..917f4dbcc211b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { AnomaliesQueryTabBodyProps } from './types'; +import { manageQuery } from '../../../components/page/manage_query'; +import { AnomaliesOverTimeHistogram } from '../../../components/anomalies_over_time'; +import { AnomaliesOverTimeQuery } from '../anomalies_over_time'; +import { getAnomaliesFilterQuery } from './utils'; +import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; + +const AnomaliesOverTimeManage = manageQuery(AnomaliesOverTimeHistogram); + +export const AnomaliesQueryTabBody = ({ + endDate, + skip, + startDate, + type, + narrowDateRange, + filterQuery, + anomaliesFilterQuery, + setQuery, + hideHistogramIfEmpty, + updateDateRange = () => {}, + AnomaliesTableComponent, + flowTarget, + ip, +}: AnomaliesQueryTabBodyProps) => { + const [siemJobsLoading, siemJobs] = useSiemJobs(true); + const [anomalyScore] = useKibanaUiSetting(DEFAULT_ANOMALY_SCORE); + + const mergedFilterQuery = getAnomaliesFilterQuery( + filterQuery, + anomaliesFilterQuery, + siemJobs, + anomalyScore, + flowTarget, + ip + ); + + return ( + <> + + {({ anomaliesOverTime, loading, id, inspect, refetch, totalCount }) => { + if (hideHistogramIfEmpty && !anomaliesOverTime.length) { + return
; + } + + return ( + <> + + + + ); + }} + + + + ); +}; + +AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts new file mode 100644 index 0000000000000..0aef02ddd929a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { SetQuery } from '../../../pages/hosts/navigation/types'; +import { FlowTarget } from '../../../graphql/types'; +import { HostsType } from '../../../store/hosts/model'; +import { NetworkType } from '../../../store/network/model'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; + +interface QueryTabBodyProps { + type: HostsType | NetworkType; + filterQuery?: string | ESTermQuery; +} + +export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { + startDate: number; + endDate: number; + skip: boolean; + setQuery: SetQuery; + narrowDateRange: NarrowDateRange; + updateDateRange?: UpdateDateRange; + anomaliesFilterQuery?: object; + hideHistogramIfEmpty?: boolean; + ip?: string; + flowTarget?: FlowTarget; + AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts new file mode 100644 index 0000000000000..9609619916ab1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 deepmerge from 'deepmerge'; +import { createFilter } from '../../helpers'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { SiemJob } from '../../../components/ml_popover/types'; +import { FlowTarget } from '../../../graphql/types'; + +export const getAnomaliesFilterQuery = ( + filterQuery: string | ESTermQuery | undefined, + anomaliesFilterQuery: object = {}, + siemJobs: SiemJob[] = [], + anomalyScore: number, + flowTarget?: FlowTarget, + ip?: string +): string => { + const siemJobIds = siemJobs + .filter(job => job.isInstalled) + .map(job => job.id) + .map(jobId => ({ + match_phrase: { + job_id: jobId, + }, + })); + + const filterQueryString = createFilter(filterQuery); + const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; + const mergedFilterQuery = deepmerge.all([ + filterQueryObject, + anomaliesFilterQuery, + { + bool: { + filter: [ + { + bool: { + should: siemJobIds, + minimum_should_match: 1, + }, + }, + { + match_phrase: { + result_type: 'record', + }, + }, + flowTarget && + ip && { + match_phrase: { + [`${flowTarget}.ip`]: ip, + }, + }, + { + range: { + record_score: { + gte: anomalyScore, + }, + }, + }, + ], + }, + }, + ]); + + return JSON.stringify(mergedFilterQuery); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts new file mode 100644 index 0000000000000..798cf91612a85 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 chrome from 'ui/chrome'; +import { + AddRulesProps, + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + NewRule, + Rule, +} from './types'; +import { throwIfNotOk } from '../../../hooks/api/api'; + +/** + * Add provided Rule + * + * @param rule to add + * @param kbnVersion current Kibana Version to use for headers + */ +export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { + const response = await fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify(rule), + signal, + }); + + await throwIfNotOk(response); + return response.json(); +}; + +/** + * Fetches all rules or single specified rule from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param id if specified, will return specific rule if exists + * @param kbnVersion current Kibana Version to use for headers + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + id, + kbnVersion, + signal, +}: FetchRulesProps): Promise => { + const queryParams = [ + `page=${pagination.page}`, + `per_page=${pagination.perPage}`, + `sort_field=${filterOptions.sortField}`, + `sort_order=${filterOptions.sortOrder}`, + ...(filterOptions.filter.length !== 0 + ? [`filter=alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] + : []), + ]; + + const endpoint = + id != null + ? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"` + : `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`; + + const response = await fetch(endpoint, { + method: 'GET', + signal, + }); + await throwIfNotOk(response); + return id != null + ? { + page: 0, + perPage: 1, + total: 1, + data: response.json(), + } + : response.json(); +}; + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * @param kbnVersion current Kibana Version to use for headers + */ +export const enableRules = async ({ + ids, + enabled, + kbnVersion, +}: EnableRulesProps): Promise => { + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'PUT', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ id, enabled }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * @param kbnVersion current Kibana Version to use for headers + */ +export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { + // TODO: Don't delete if immutable! + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, { + method: 'DELETE', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Duplicates provided Rules + * + * @param rule to duplicate + * @param kbnVersion current Kibana Version to use for headers + */ +export const duplicateRules = async ({ + rules, + kbnVersion, +}: DuplicateRulesProps): Promise => { + const requests = rules.map(rule => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ + ...rule, + name: `${rule.name} [Duplicate]`, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_by: undefined, + enabled: rule.enabled, + }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx new file mode 100644 index 0000000000000..dbc148a96365d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, get } from 'lodash/fp'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { getIndexFields, sourceQuery } from '../../../containers/source'; +import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { SourceQuery } from '../../../graphql/types'; +import { useApolloClient } from '../../../utils/apollo_context'; + +import * as i18n from './translations'; + +interface FetchIndexPattern { + isLoading: boolean; + indices: string[]; + indicesExists: boolean; + indexPatterns: StaticIndexPattern | null; +} + +type Return = [FetchIndexPattern, Dispatch>]; + +export const useFetchIndexPatterns = (): Return => { + const apolloClient = useApolloClient(); + const [indices, setIndices] = useState([]); + const [indicesExists, setIndicesExists] = useState(false); + const [indexPatterns, setIndexPatterns] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + async function fetchIndexPatterns() { + if (apolloClient && !isEmpty(indices)) { + setIsLoading(true); + apolloClient + .query({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId: 'default', + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal: abortCtrl.signal, + }, + }, + }) + .then( + result => { + if (isSubscribed) { + setIsLoading(false); + setIndicesExists(get('data.source.status.indicesExist', result)); + setIndexPatterns( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + } + }, + error => { + if (isSubscribed) { + setIsLoading(false); + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + ); + } + } + fetchIndexPatterns(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [indices]); + + return [{ isLoading, indices, indicesExists, indexPatterns }, setIndices]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx new file mode 100644 index 0000000000000..371d28aebf7f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, Dispatch } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; + +import { addRule as persistRule } from './api'; +import * as i18n from './translations'; +import { NewRule } from './types'; + +interface PersistRuleReturn { + isLoading: boolean; + isSaved: boolean; +} + +type Return = [PersistRuleReturn, Dispatch]; + +export const usePersistRule = (): Return => { + const [rule, setRule] = useState(null); + const [isSaved, setIsSaved] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setIsSaved(false); + async function saveRule() { + if (rule != null) { + try { + setIsLoading(true); + await persistRule({ rule, kbnVersion, signal: abortCtrl.signal }); + + if (isSubscribed) { + setIsSaved(true); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + } + + saveRule(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [rule]); + + return [{ isLoading, isSaved }, setRule]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts new file mode 100644 index 0000000000000..39efbde2ad5c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_FETCH_FAILURE = i18n.translate('xpack.siem.containers.detectionEngine.rules', { + defaultMessage: 'Failed to fetch Rules', +}); + +export const RULE_ADD_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.addRuleFailDescription', + { + defaultMessage: 'Failed to add Rule', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..fe6fb04800adc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const NewRuleSchema = t.intersection([ + t.type({ + description: t.string, + enabled: t.boolean, + index: t.array(t.string), + interval: t.string, + language: t.string, + name: t.string, + query: t.string, + severity: t.string, + type: t.union([t.literal('query'), t.literal('saved_query')]), + }), + t.partial({ + created_by: t.string, + false_positives: t.array(t.string), + from: t.string, + id: t.string, + max_signals: t.number, + references: t.array(t.string), + rule_id: t.string, + tags: t.array(t.string), + to: t.string, + updated_by: t.string, + }), +]); + +export const NewRulesSchema = t.array(NewRuleSchema); +export type NewRule = t.TypeOf; + +export interface AddRulesProps { + rule: NewRule; + kbnVersion: string; + signal: AbortSignal; +} + +export const RuleSchema = t.intersection([ + t.type({ + created_by: t.string, + description: t.string, + enabled: t.boolean, + id: t.string, + index: t.array(t.string), + interval: t.string, + language: t.string, + name: t.string, + query: t.string, + rule_id: t.string, + severity: t.string, + type: t.string, + updated_by: t.string, + }), + t.partial({ + false_positives: t.array(t.string), + from: t.string, + max_signals: t.number, + references: t.array(t.string), + tags: t.array(t.string), + to: t.string, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf; +export type Rules = t.TypeOf; + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + id?: string; + kbnVersion: string; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; + kbnVersion: string; +} + +export interface DeleteRulesProps { + ids: string[]; + kbnVersion: string; +} + +export interface DuplicateRulesProps { + rules: Rules; + kbnVersion: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx new file mode 100644 index 0000000000000..2b8bb986a296a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; +import { useStateToaster } from '../../../components/toasters'; +import { fetchRules } from './api'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; + +type Return = [boolean, FetchRulesResponse]; + +/** + * Hook for using the list of Rules from the Detection Engine API + * + * @param pagination desired pagination options (e.g. page/perPage) + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param refetchToggle toggle for refetching data + */ +export const useRules = ( + pagination: PaginationOptions, + filterOptions: FilterOptions, + refetchToggle: boolean +): Return => { + const [rules, setRules] = useState({ + page: 1, + perPage: 20, + total: 0, + data: [], + }); + const [loading, setLoading] = useState(true); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchData() { + try { + const fetchRulesResult = await fetchRules({ + filterOptions, + pagination, + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setRules(fetchRulesResult); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + refetchToggle, + pagination.page, + pagination.perPage, + filterOptions.filter, + filterOptions.sortField, + filterOptions.sortOrder, + ]); + + return [loading, rules]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts index 365d93ee7e756..da83e09e4629a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts @@ -50,6 +50,11 @@ export const networkDnsQuery = gql` dsl response } + histogram { + x + y + g + } } } } diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx index af6806824d338..592fe43b9873f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx @@ -16,15 +16,17 @@ import { NetworkDnsEdges, NetworkDnsSortField, PageInfoPaginated, + MatrixOverOrdinalHistogramData, } from '../../graphql/types'; import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; import { createFilter, getDefaultFetchPolicy } from '../helpers'; import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; import { networkDnsQuery } from './index.gql_query'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; const ID = 'networkDnsQuery'; - +const HISTOGRAM_ID = 'networkDnsHistogramQuery'; export interface NetworkDnsArgs { id: string; inspect: inputsModel.InspectQuery; @@ -35,6 +37,7 @@ export interface NetworkDnsArgs { pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; totalCount: number; + histogram: MatrixOverOrdinalHistogramData[]; } export interface OwnProps extends QueryTemplatePaginatedProps { @@ -52,7 +55,7 @@ export interface NetworkDnsComponentReduxProps { type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps; -class NetworkDnsComponentQuery extends QueryTemplatePaginated< +export class NetworkDnsComponentQuery extends QueryTemplatePaginated< NetworkDnsProps, GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables @@ -129,6 +132,7 @@ class NetworkDnsComponentQuery extends QueryTemplatePaginated< pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), refetch: this.memoizedRefetchQuery(variables, limit, refetch), totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), + histogram: getOr(null, 'source.NetworkDns.histogram', data), }); }} @@ -144,6 +148,24 @@ const makeMapStateToProps = () => { return { ...getNetworkDnsSelector(state), isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, }; }; @@ -151,3 +173,6 @@ const makeMapStateToProps = () => { }; export const NetworkDnsQuery = connect(makeMapStateToProps)(NetworkDnsComponentQuery); +export const NetworkDnsHistogramQuery = connect(makeMapHistogramStateToProps)( + NetworkDnsComponentQuery +); diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index ff6e5e4d0c788..bc7b87cda6af9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -7,7 +7,7 @@ import { isUndefined } from 'lodash'; import { get, keyBy, pick, set } from 'lodash/fp'; import { Query } from 'react-apollo'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import memoizeOne from 'memoize-one'; import { StaticIndexPattern } from 'ui/index_patterns'; import chrome from 'ui/chrome'; @@ -16,6 +16,9 @@ import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { IndexField, SourceQuery } from '../../graphql/types'; import { sourceQuery } from './index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; + +export { sourceQuery }; export interface BrowserField { aggregatable: boolean; @@ -57,7 +60,7 @@ interface WithSourceProps { sourceId: string; } -const getIndexFields = memoizeOne( +export const getIndexFields = memoizeOne( (title: string, fields: IndexField[]): StaticIndexPattern => fields && fields.length > 0 ? { @@ -110,3 +113,56 @@ WithSource.displayName = 'WithSource'; export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => indicesExist || isUndefined(indicesExist); + +export const useWithSource = (sourceId: string, indices: string[]) => { + const [loading, updateLoading] = useState(false); + const [indicesExist, setIndicesExist] = useState(undefined); + const [browserFields, setBrowserFields] = useState(null); + const [indexPattern, setIndexPattern] = useState(null); + const [errorMessage, updateErrorMessage] = useState(null); + + const apolloClient = useApolloClient(); + async function fetchSource(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateErrorMessage(null); + setIndicesExist(get('data.source.status.indicesExist', result)); + setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); + setIndexPattern( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + }, + error => { + updateLoading(false); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchSource(signal); + return () => abortCtrl.abort(); + }, [apolloClient, sourceId, indices]); + + return { indicesExist, browserFields, indexPattern, loading, errorMessage }; +}; diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 9bde4bf47fff0..7c173a9a90626 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -666,6 +666,53 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "AnomaliesOverTime", + "description": "", + "args": [ + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "AnomaliesOverTimeData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "Authentications", "description": "Gets Authentication success and failures based on a timerange", @@ -2491,6 +2538,159 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "AnomaliesOverTimeData", + "description": "", + "fields": [ + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anomaliesOverTime", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Inspect", + "description": "", + "fields": [ + { + "name": "dsl", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "response", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PaginationInputPaginated", @@ -3200,57 +3400,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "Inspect", - "description": "", - "fields": [ - { - "name": "dsl", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "AuthenticationsOverTimeData", @@ -3306,53 +3455,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "g", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "PaginationInput", @@ -8034,6 +8136,26 @@ "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "histogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverOrdinalHistogramData", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -8135,6 +8257,53 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "MatrixOverOrdinalHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "NetworkHttpSortField", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 833102a0d00bc..1464b55648035 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -456,6 +456,8 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; + + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -556,6 +558,28 @@ export interface IndexField { format?: Maybe; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface MatrixOverTimeHistogramData { + x: number; + + y: number; + + g: string; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -690,12 +714,6 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface Inspect { - dsl: string[]; - - response: string[]; -} - export interface AuthenticationsOverTimeData { inspect?: Maybe; @@ -704,14 +722,6 @@ export interface AuthenticationsOverTimeData { totalCount: number; } -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - export interface TimelineData { edges: TimelineEdges[]; @@ -1626,6 +1636,8 @@ export interface NetworkDnsData { pageInfo: PageInfoPaginated; inspect?: Maybe; + + histogram?: Maybe; } export interface NetworkDnsEdges { @@ -1648,6 +1660,14 @@ export interface NetworkDnsItem { uniqueDomains?: Maybe; } +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + export interface NetworkHttpData { edges: NetworkHttpEdges[]; @@ -2117,6 +2137,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AnomaliesOverTimeSourceArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; +} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2411,6 +2438,58 @@ export interface DeleteTimelineMutationArgs { // Documents // ==================================================== +export namespace GetAnomaliesOverTimeQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + defaultIndex: string[]; + filterQuery?: Maybe; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + AnomaliesOverTime: AnomaliesOverTime; + }; + + export type AnomaliesOverTime = { + __typename?: 'AnomaliesOverTimeData'; + + anomaliesOverTime: _AnomaliesOverTime[]; + + totalCount: number; + + inspect: Maybe; + }; + + export type _AnomaliesOverTime = { + __typename?: 'MatrixOverTimeHistogramData'; + + x: number; + + y: number; + + g: string; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + export namespace GetAuthenticationsOverTimeQuery { export type Variables = { sourceId: string; @@ -3311,6 +3390,8 @@ export namespace GetNetworkDnsQuery { pageInfo: PageInfo; inspect: Maybe; + + histogram: Maybe; }; export type Edges = { @@ -3360,6 +3441,16 @@ export namespace GetNetworkDnsQuery { response: string[]; }; + + export type Histogram = { + __typename?: 'MatrixOverOrdinalHistogramData'; + + x: string; + + y: number; + + g: string; + }; } export namespace GetNetworkHttpQuery { diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index b6819e54575d6..d82079dd05d31 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { Query, esFilters, esQuery, + esKuery, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; @@ -21,7 +21,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; @@ -31,10 +33,10 @@ export const convertKueryToElasticSearchQuery = ( export const convertKueryToDslFilter = ( kueryExpression: string, indexPattern: IIndexPattern -): JsonObject => { +): esKuery.JsonObject => { try { return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) : {}; } catch (err) { return {}; @@ -55,7 +57,7 @@ export const escapeQueryValue = (val: number | string = ''): string | number => export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { try { - fromKueryExpression(kqlFilterQuery.expression); + esKuery.fromKueryExpression(kqlFilterQuery.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx new file mode 100644 index 0000000000000..b1defcb34066d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { useKibanaUiSetting } from '../settings/use_kibana_ui_setting'; + +export const useEuiTheme = () => { + const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE); + return darkMode ? darkTheme : lightTheme; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx new file mode 100644 index 0000000000000..66353a9613650 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React, { memo } from 'react'; + +import { RuleStatusIcon, RuleStatusIconProps } from '../status_icon'; + +interface AccordionTitleProps extends RuleStatusIconProps { + title: string; +} + +export const AccordionTitle = memo(({ name, title, type }) => ( + + + + + + +
{title}
+
+
+
+)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx new file mode 100644 index 0000000000000..6673262a15906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { isEmpty, isEqual } from 'lodash/fp'; +import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import * as I18n from './translations'; + +interface AddItemProps { + addText: string; + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [items, setItems] = useState(['']); + const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(false); + + const lastInputRef = useRef(null); + + const removeItem = useCallback( + (index: number) => { + const values = field.value as string[]; + field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + }, + [field] + ); + + const addItem = useCallback(() => { + const values = field.value as string[]; + if (!isEmpty(values[values.length - 1])) { + field.setValue([...values, '']); + } + }, [field]); + + const updateItem = useCallback( + (event: ChangeEvent, index: number) => { + const values = field.value as string[]; + const value = event.target.value; + if (isEmpty(value)) { + setHaveBeenKeyboardDeleted(true); + field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + } else { + field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); + } + }, + [field] + ); + + const handleLastInputRef = useCallback( + (element: HTMLInputElement | null) => { + lastInputRef.current = element; + }, + [lastInputRef] + ); + + useEffect(() => { + if (!isEqual(field.value, items)) { + setItems( + isEmpty(field.value) + ? [''] + : haveBeenKeyboardDeleted + ? [...(field.value as string[]), ''] + : (field.value as string[]) + ); + setHaveBeenKeyboardDeleted(false); + } + }, [field.value]); + + useEffect(() => { + if (!haveBeenKeyboardDeleted && lastInputRef != null && lastInputRef.current != null) { + lastInputRef.current.focus(); + } + }, [haveBeenKeyboardDeleted, lastInputRef]); + + return ( + + <> + {items.map((item, index) => { + const euiFieldProps = { + disabled: isDisabled, + ...(index === items.length - 1 ? { inputRef: handleLastInputRef } : {}), + }; + return ( +
+ removeItem(index)} + aria-label={I18n.DELETE} + /> + } + value={item} + onChange={e => updateItem(e, index)} + compressed + fullWidth + {...euiFieldProps} + /> + {items.length - 1 !== index && } +
+ ); + })} + + + {addText} + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts new file mode 100644 index 0000000000000..98c15606d88fe --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const DELETE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.addItem.deleteDescription', + { + defaultMessage: 'Delete', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx new file mode 100644 index 0000000000000..4e7832c890255 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { useCallback, useEffect, useState } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; + +import { SavedQueryTimeFilter } from '../../../../../../../../../../src/legacy/core_plugins/data/public/search'; +import { SavedQuery } from '../../../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esFilters, + Query, + FilterManager, +} from '../../../../../../../../../../src/plugins/data/public'; + +import { QueryBar } from '../../../../../components/query_bar'; +import { useKibanaCore } from '../../../../../lib/compose/kibana_core'; +import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +export interface FieldValueQueryBar { + filters: esFilters.Filter[]; + query: Query; + saved_id: string; +} +interface QueryBarDefineRuleProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isLoading: boolean; + indexPattern: StaticIndexPattern; +} + +const StyledEuiFormRow = styled(EuiFormRow)` + .kbnTypeahead__items { + max-height: 14vh !important; + } + .globalQueryBar { + padding: 4px 0px 0px 0px; + .kbnQueryBar { + & > div:first-child { + margin: 0px 0px 0px 4px; + } + } + } +`; + +// TODO need to add disabled in the SearchBar + +export const QueryBarDefineRule = ({ + dataTestSubj, + field, + idAria, + indexPattern, + isLoading = false, +}: QueryBarDefineRuleProps) => { + const [savedQuery, setSavedQuery] = useState(null); + const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const core = useKibanaCore(); + const [filterManager] = useState(new FilterManager(core.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters([]); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const newFilters = filterManager.getFilters(); + const { filters } = field.value as FieldValueQueryBar; + + if (!isEqual(filters, newFilters)) { + field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + } + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, [field.value]); + + useEffect(() => { + let isSubscribed = true; + async function updateFilterQueryFromValue() { + const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; + if (!isEqual(query, queryDraft)) { + setQueryDraft(query); + } + if (!isEqual(filters, filterManager.getFilters())) { + filterManager.setFilters(filters); + } + if ( + (savedId != null && savedQuery != null && savedId !== savedQuery.id) || + (savedId != null && savedQuery == null) + ) { + try { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery(mySavedQuery); + } + } catch { + setSavedQuery(null); + } + } else if (savedId == null && savedQuery != null) { + setSavedQuery(null); + } + } + updateFilterQueryFromValue(); + return () => { + isSubscribed = false; + }; + }, [field.value]); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + const { query } = field.value as FieldValueQueryBar; + if (!isEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + const { query } = field.value as FieldValueQueryBar; + if (!isEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + const { saved_id: savedId } = field.value as FieldValueQueryBar; + if (newSavedQuery.id !== savedId) { + setSavedQuery(newSavedQuery); + field.setValue({ + filters: newSavedQuery.attributes.filters, + query: newSavedQuery.attributes.query, + saved_id: newSavedQuery.id, + }); + } + } + }, + [field.value] + ); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx new file mode 100644 index 0000000000000..ebb365f6087a9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +import * as I18n from './translations'; + +interface ScheduleItemProps { + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +const timeTypeOptions = [ + { value: 's', text: I18n.SECONDS }, + { value: 'm', text: I18n.MINUTES }, + { value: 'h', text: I18n.HOURS }, +]; + +const StyledEuiFormRow = styled(EuiFormRow)` + .euiFormControlLayout { + max-width: 200px !important; + } +`; + +export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => { + const [timeType, setTimeType] = useState('s'); + const [timeVal, setTimeVal] = useState(0); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChangeTimeType = useCallback(e => { + setTimeType(e.target.value); + }, []); + + const onChangeTimeVal = useCallback(e => { + const sanitizedValue: number = parseInt(e.target.value, 10); + setTimeVal(isNaN(sanitizedValue) ? 0 : sanitizedValue); + }, []); + + useEffect(() => { + if (!isEmpty(timeVal) && Number(timeVal) >= 0 && field.value !== `${timeVal}${timeType}`) { + field.setValue(`${timeVal}${timeType}`); + } + }, [field.value, timeType, timeVal]); + + useEffect(() => { + if (!isEmpty(field.value)) { + const filterTimeVal = (field.value as string).match(/\d+/g); + const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g); + if ( + !isEmpty(filterTimeVal) && + filterTimeVal != null && + !isNaN(Number(filterTimeVal[0])) && + Number(filterTimeVal[0]) !== Number(timeVal) + ) { + setTimeVal(Number(filterTimeVal[0])); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) && + filterTimeType[0] !== timeType + ) { + setTimeType(filterTimeType[0]); + } + } + }, [field.value]); + + // EUI missing some props + const rest = { disabled: isDisabled }; + + return ( + + + } + compressed + fullWidth + min={0} + onChange={onChangeTimeVal} + value={timeVal} + {...rest} + /> + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts new file mode 100644 index 0000000000000..1bc983814c330 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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'; + +export const SECONDS = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.secondsOptionDescription', + { + defaultMessage: 'Seconds', + } +); + +export const MINUTES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.minutesOptionDescription', + { + defaultMessage: 'Minutes', + } +); + +export const HOURS = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.hoursOptionDescription', + { + defaultMessage: 'Hours', + } +); + +export const INVALID_TIME = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.invalidTimeMessageDescription', + { + defaultMessage: 'A time is required.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts new file mode 100644 index 0000000000000..6c91c4a02edf9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + Form, + FormDataProvider, + UseField, + useForm, +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx new file mode 100644 index 0000000000000..ad0011ff8ed18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiAvatar, EuiIcon } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { useEuiTheme } from '../../../../../lib/theme/use_eui_theme'; + +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStatusIconProps { + name: string; + type: RuleStatusType; +} + +const RuleStatusIconStyled = styled.div` + position: relative; + svg { + position: absolute; + top: 8px; + left: 9px; + } +`; + +export const RuleStatusIcon = memo(({ name, type }) => { + const theme = useEuiTheme(); + const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorDarkestShade; + return ( + + + {type === 'valid' ? : null} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts new file mode 100644 index 0000000000000..7d6e434bcc8c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as I18n from './translations'; + +export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; + +interface SeverityOptionItem { + value: SeverityValue; + text: string; +} + +export const severityOptions: SeverityOptionItem[] = [ + { value: 'low', text: I18n.LOW }, + { value: 'medium', text: I18n.MEDIUM }, + { value: 'high', text: I18n.HIGH }, + { value: 'critical', text: I18n.CRITICAL }, +]; + +export const defaultRiskScoreBySeverity: Record = { + low: 21, + medium: 47, + high: 73, + critical: 99, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts new file mode 100644 index 0000000000000..b94fa8c933937 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const defaultValue = { + name: '', + description: '', + severity: 'low', + riskScore: 50, + references: [], + falsePositives: [], + tags: [], +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx new file mode 100644 index 0000000000000..4393f39ad2f85 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; + +import { RuleStepProps, RuleStep } from '../../types'; +import * as CreateRuleI18n from '../../translations'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { AddItem } from '../add_item_form'; +import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; +import { defaultValue } from './default_value'; +import { schema } from './schema'; +import * as I18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +export const StepAboutRule = memo(({ isLoading, setStepData }) => { + const { form } = useForm({ + schema, + defaultValue, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback(async () => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.aboutRule, data, newIsValid); + } + }, [form]); + + return ( + <> + + + + + + + + + + {({ severity }) => { + const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const riskScoreField = form.getFields().riskScore; + if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + riskScoreField.setValue(newRiskScore); + } + return null; + }} + + + + + + + {CreateRuleI18n.CONTINUE} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx new file mode 100644 index 0000000000000..97ad3d595a938 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FormSchema, + FIELD_TYPES, +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +import * as CreateRuleI18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldNameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldDescriptionLabel', + { + defaultMessage: 'Description', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } + ) + ), + }, + ], + }, + severity: { + type: FIELD_TYPES.SELECT, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', + { + defaultMessage: 'Severity', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', + { + defaultMessage: 'A severity is required.', + } + ) + ), + }, + ], + }, + riskScore: { + type: FIELD_TYPES.RANGE, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', + { + defaultMessage: 'Risk score', + } + ), + }, + references: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', + { + defaultMessage: 'Reference URLs', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, + falsePositives: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', + { + defaultMessage: 'False positives', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { + defaultMessage: 'Tags', + }), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts new file mode 100644 index 0000000000000..bd759b345d70d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_REFERENCE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription', + { + defaultMessage: 'Add reference', + } +); + +export const ADD_FALSE_POSITIVE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription', + { + defaultMessage: 'Add false positive', + } +); + +export const LOW = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', + { + defaultMessage: 'Low', + } +); + +export const MEDIUM = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription', + { + defaultMessage: 'Medium', + } +); + +export const HIGH = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription', + { + defaultMessage: 'High', + } +); + +export const CRITICAL = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionCriticalDescription', + { + defaultMessage: 'Critical', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx new file mode 100644 index 0000000000000..b09d0df962793 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { DEFAULT_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY } from '../../../../../../common/constants'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import * as CreateRuleI18n from '../../translations'; +import { RuleStep, RuleStepProps } from '../../types'; +import { QueryBarDefineRule } from '../query_bar'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { schema } from './schema'; +import * as I18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +export const StepDefineRule = memo(({ isLoading, setStepData }) => { + const [initializeOutputIndex, setInitializeOutputIndex] = useState(true); + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [ + { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + setIndices, + ] = useFetchIndexPatterns(); + const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); + const [signalIndexConfig] = useKibanaUiSetting(DEFAULT_SIGNALS_INDEX_KEY); + + const { form } = useForm({ + schema, + defaultValue: { + index: indicesConfig || [], + outputIndex: signalIndexConfig, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', + }, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback(async () => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.defineRule, data, newIsValid); + } + }, [form]); + + useEffect(() => { + if (signalIndexConfig != null && initializeOutputIndex) { + const outputIndexField = form.getFields().outputIndex; + outputIndexField.setValue(signalIndexConfig); + setInitializeOutputIndex(false); + } + }, [initializeOutputIndex, signalIndexConfig, form]); + + return ( + <> +
+ + + + + + {({ useIndicesConfig }) => { + if (localUseIndicesConfig !== useIndicesConfig) { + const indexField = form.getFields().index; + if ( + indexField != null && + useIndicesConfig === 'true' && + !isEqual(indexField.value, indicesConfig) + ) { + indexField.setValue(indicesConfig); + setIndices(indicesConfig); + } else if ( + indexField != null && + useIndicesConfig === 'false' && + !isEqual(indexField.value, []) + ) { + indexField.setValue([]); + setIndices([]); + } + setLocalUseIndicesConfig(useIndicesConfig); + } + + return null; + }} + + + + + + + {CreateRuleI18n.CONTINUE} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx new file mode 100644 index 0000000000000..58a9e57b32ce6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { + FormSchema, + FIELD_TYPES, + ValidationFunc, +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; + +import * as CreateRuleI18n from '../../translations'; + +import { FieldValueQueryBar } from '../query_bar'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + outputIndex: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldOutputIndiceNameLabel', + { + defaultMessage: 'Output index name', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'An output indice name for signals is required.', + } + ) + ), + }, + ], + }, + useIndicesConfig: { + type: FIELD_TYPES.RADIO_GROUP, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldIndicesTypeLabel', + { + defaultMessage: 'Indices type', + } + ), + }, + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', { + defaultMessage: 'Indices', + }), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'An output indice name for signals is required.', + } + ) + ), + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + const { query, filters } = value as FieldValueQueryBar; + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + const { query } = value as FieldValueQueryBar; + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + esKuery.fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + return undefined; + }, + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx new file mode 100644 index 0000000000000..0050c59a4a2c8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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'; + +export const CUSTOM_QUERY_REQUIRED = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError', + { + defaultMessage: 'A custom query is required.', + } +); + +export const INVALID_CUSTOM_QUERY = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError', + { + defaultMessage: 'The KQL is invalid', + } +); + +export const CONFIG_INDICES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription', + { + defaultMessage: 'Use Elasticsearch indices from SIEM advanced settings', + } +); + +export const CUSTOM_INDICES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesCustomDescription', + { + defaultMessage: 'Provide custom list of indices', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts new file mode 100644 index 0000000000000..df52b0c9ff64e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueQueryBar } from '../query_bar'; + +export interface QueryBarStepDefineRule { + queryBar: FieldValueQueryBar; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx new file mode 100644 index 0000000000000..10b95ac6c8742 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; + +import { RuleStep, RuleStepProps } from '../../types'; +import { ScheduleItem } from '../schedule_item_form'; +import { Form, UseField, useForm } from '../shared_imports'; +import { schema } from './schema'; +import * as I18n from './translations'; + +export const StepScheduleRule = memo(({ isLoading, setStepData }) => { + const { form } = useForm({ + schema, + defaultValue: { + interval: '5m', + from: '0m', + }, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback( + async (enabled: boolean) => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + } + }, + [form] + ); + + return ( + <> +
+ + + + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx new file mode 100644 index 0000000000000..6192a3b905879 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FormSchema } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +import * as CreateRuleI18n from '../../translations'; + +export const schema: FormSchema = { + interval: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', + { + defaultMessage: 'Rule run interval & look-back', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', + { + defaultMessage: 'How often and how far back this rule will search specified indices.', + } + ), + }, + from: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', + { + defaultMessage: 'Additional look-back', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', + { + defaultMessage: + 'Add more time to the look-back range in order to prevent potential gaps in signal reporting.', + } + ), + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx new file mode 100644 index 0000000000000..feaaf4e85b2af --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( + 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', + { + defaultMessage: 'Complete rule without activating', + } +); + +export const COMPLETE_WITH_ACTIVATING = i18n.translate( + 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', + { + defaultMessage: 'Complete rule & activate', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts new file mode 100644 index 0000000000000..b864260dd3338 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import moment from 'moment'; + +import { NewRule } from '../../../containers/detection_engine/rules/types'; + +import { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + FormatRuleType, +} from './types'; + +const getTimeTypeValue = (time: string): { unit: string; value: number } => { + const timeObj = { + unit: '', + value: 0, + }; + const filterTimeVal = (time as string).match(/\d+/g); + const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { + timeObj.value = Number(filterTimeVal[0]); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) + ) { + timeObj.unit = filterTimeType[0]; + } + return timeObj; +}; + +const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { + const { queryBar, useIndicesConfig, outputIndex, ...rest } = defineStepData; + const { filters, query, saved_id: savedId } = queryBar; + return { + ...rest, + language: query.language, + filters, + output_index: outputIndex, + query: query.query as string, + ...(savedId != null ? { saved_id: savedId } : {}), + }; +}; + +const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { + const formatScheduleData = scheduleData; + + if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( + formatScheduleData.interval + ); + const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); + formatScheduleData.from = `now-${duration.asSeconds()}s`; + formatScheduleData.to = 'now'; + } + return formatScheduleData; +}; + +const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { falsePositives, references, riskScore, ...rest } = aboutStepData; + + return { + false_positives: falsePositives.filter(item => !isEmpty(item)), + references: references.filter(item => !isEmpty(item)), + risk_score: riskScore, + ...rest, + }; +}; + +export const formatRule = ( + defineStepData: DefineStepRule, + aboutStepData: AboutStepRule, + scheduleData: ScheduleStepRule +): NewRule => { + const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query'; + const persistData = { + type, + ...formatDefineStepData(defineStepData), + ...formatAboutStepData(aboutStepData), + ...formatScheduleStepData(scheduleData), + meta: { + from: scheduleData.from, + }, + }; + + return persistData; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx index 47a3527aff99c..c505124c25039 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx @@ -4,22 +4,189 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useRef, useState } from 'react'; +import { Redirect } from 'react-router-dom'; import { HeaderPage } from '../../../components/header_page'; import { WrapperPage } from '../../../components/wrapper_page'; +import { AccordionTitle } from './components/accordion_title'; +import { StepAboutRule } from './components/step_about_rule'; +import { StepDefineRule } from './components/step_define_rule'; +import { StepScheduleRule } from './components/step_schedule_rule'; +import { usePersistRule } from '../../../containers/detection_engine/rules/persist_rule'; import { SpyRoute } from '../../../utils/route/spy_routes'; + +import { formatRule } from './helpers'; import * as i18n from './translations'; +import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from './types'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; + +const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; export const CreateRuleComponent = React.memo(() => { + const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const defineRuleRef = useRef(null); + const aboutRuleRef = useRef(null); + const scheduleRuleRef = useRef(null); + const stepsData = useRef>({ + [RuleStep.defineRule]: { isValid: false, data: {} }, + [RuleStep.aboutRule]: { isValid: false, data: {} }, + [RuleStep.scheduleRule]: { isValid: false, data: {} }, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + + const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => { + stepsData.current[step] = { data, isValid }; + if (isValid) { + const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); + if ([0, 1].includes(stepRuleIdx)) { + openCloseAccordion(step); + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } else if ( + stepRuleIdx === 2 && + stepsData.current[RuleStep.defineRule].isValid && + stepsData.current[RuleStep.aboutRule].isValid + ) { + setRule( + formatRule( + stepsData.current[RuleStep.defineRule].data as DefineStepRule, + stepsData.current[RuleStep.aboutRule].data as AboutStepRule, + stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule + ) + ); + } + } + }; + + const getAccordionType = useCallback( + (accordionId: RuleStep) => { + if (accordionId === openAccordionId) { + return 'active'; + } else if (stepsData.current[accordionId].isValid) { + return 'valid'; + } + return 'passive'; + }, + [openAccordionId, stepsData.current] + ); + + const defineRuleButton = ( + + ); + + const aboutRuleButton = ( + + ); + + const scheduleRuleButton = ( + + ); + + const openCloseAccordion = (accordionId: RuleStep | null) => { + if (accordionId != null) { + if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) { + defineRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) { + aboutRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) { + scheduleRuleRef.current.onToggle(); + } + } + }; + + const manageAccordions = useCallback( + (id: RuleStep, isOpen: boolean) => { + const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); + const isLatestStepsRuleValid = + stepRuleIdx === 0 + ? true + : stepsRuleOrder + .filter((stepRule, index) => index < stepRuleIdx) + .every(stepRule => stepsData.current[stepRule].isValid); + + if ( + openAccordionId != null && + openAccordionId !== id && + !stepsData.current[openAccordionId].isValid && + isOpen + ) { + openCloseAccordion(id); + } else if (!isLatestStepsRuleValid && isOpen) { + openCloseAccordion(id); + } else if (openAccordionId != null && id !== openAccordionId && isOpen) { + openCloseAccordion(openAccordionId); + setOpenAccordionId(id); + } else if (openAccordionId == null && isOpen) { + setOpenAccordionId(id); + } + }, + [openAccordionId] + ); + + if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) { + return ; + } + return ( <> + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts index 884f3f3741228..ca96566305a6b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts @@ -9,3 +9,32 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { defaultMessage: 'Create new rule', }); + +export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.defineRuleTitle', { + defaultMessage: 'Define Rule', +}); + +export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.aboutRuleTitle', { + defaultMessage: 'About Rule', +}); + +export const SCHEDULE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.scheduleRuleTitle', + { + defaultMessage: 'Schedule Rule', + } +); + +export const OPTIONAL_FIELD = i18n.translate( + 'xpack.siem.detectionEngine.createRule.optionalFieldDescription', + { + defaultMessage: 'Optional', + } +); + +export const CONTINUE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.continueButtonTitle', + { + defaultMessage: 'Continue', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts new file mode 100644 index 0000000000000..a03f6a0b11bee --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueQueryBar } from './components/query_bar'; +import { esFilters } from '../../../../../../../../src/plugins/data/common'; + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', +} + +export interface RuleStepData { + isValid: boolean; + data: unknown; +} + +export interface RuleStepProps { + setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void; + isLoading: boolean; +} + +export interface DefineStepRule { + outputIndex: string; + useIndicesConfig: string; + index: string[]; + queryBar: FieldValueQueryBar; +} + +export interface DefineStepRuleJson { + output_index: string; + index: string[]; + filters: esFilters.Filter[]; + saved_id?: string; + query: string; + language: string; +} + +export interface AboutStepRule { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; +} + +export interface ScheduleStepRule { + enabled: boolean; + interval: string; + from: string; + to?: string; +} +export type ScheduleStepRuleJson = ScheduleStepRule; + +export type FormatRuleType = 'query' | 'saved_query'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 9b63a6e160e42..f02e80ebfaf66 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -48,7 +48,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

} + popoverContent={() =>

{'Batch actions context menu here.'}

} > {'Batch actions'}
@@ -70,7 +70,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
@@ -100,7 +100,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx index da3e5fb2083dd..b16036e3142fc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx @@ -66,7 +66,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

} + popoverContent={() =>

{'Batch actions context menu here.'}

} > {'Batch actions'}
@@ -88,7 +88,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
@@ -118,7 +118,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

} + popoverContent={() =>

{'Customize columns context menu here.'}

} > {'Customize columns'}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx new file mode 100644 index 0000000000000..58e2b9f0cabc7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { ColumnTypes } from './index'; + +const actions = [ + { + available: (item: ColumnTypes) => item.status === 'Running', + description: 'Stop', + icon: 'stop', + isPrimary: true, + name: 'Stop', + onClick: () => {}, + type: 'icon', + }, + { + available: (item: ColumnTypes) => item.status === 'Stopped', + description: 'Resume', + icon: 'play', + isPrimary: true, + name: 'Resume', + onClick: () => {}, + type: 'icon', + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const columns = [ + { + field: 'rule', + name: 'Rule', + render: (value: ColumnTypes['rule']) => {value.name}, + sortable: true, + truncateText: true, + }, + { + field: 'ran', + name: 'Ran', + render: (value: ColumnTypes['ran']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'lookedBackTo', + name: 'Looked back to', + render: (value: ColumnTypes['lookedBackTo']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'status', + name: 'Status', + sortable: true, + truncateText: true, + }, + { + field: 'response', + name: 'Response', + render: (value: ColumnTypes['response']) => { + return value === undefined ? ( + getEmptyTagValue() + ) : ( + <> + {value === 'Fail' ? ( + + {value} + + ) : ( + {value} + )} + + ); + }, + sortable: true, + truncateText: true, + }, + { + actions, + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx new file mode 100644 index 0000000000000..d7306b8630bc2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx @@ -0,0 +1,337 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { columns } from './columns'; + +export interface RuleTypes { + href: string; + name: string; +} + +export interface ColumnTypes { + id: number; + rule: RuleTypes; + ran: string; + lookedBackTo: string; + status: string; + response: string | undefined; +} + +export interface PageTypes { + index: number; + size: number; +} + +export interface SortTypes { + field: string; + direction: string; +} + +export const ActivityMonitor = React.memo(() => { + const sampleTableData = [ + { + id: 1, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Running', + }, + { + id: 2, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Stopped', + }, + { + id: 3, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Fail', + }, + { + id: 4, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 5, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 6, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 7, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 8, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 9, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 10, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 11, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 12, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 13, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 14, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 15, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 16, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 17, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 18, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 19, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 20, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 21, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + ]; + + const [itemsTotalState] = useState(sampleTableData.length); + const [pageState, setPageState] = useState({ index: 0, size: 20 }); + // const [selectedState, setSelectedState] = useState([]); + const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); + + return ( + <> + + + + + + + + + {'Showing: 39 activites'} + + + + {'Selected: 2 activities'} + + {'Stop selected'} + + + + {'Clear 7 filters'} + + + + + { + setPageState(page); + setSortState(sort); + }} + pagination={{ + pageIndex: pageState.index, + pageSize: pageState.size, + totalItemCount: itemsTotalState, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: ColumnTypes) => item.status !== 'Completed', + selectableMessage: (selectable: boolean) => + selectable ? undefined : 'Completed runs cannot be acted upon', + onSelectionChange: (selectedItems: ColumnTypes[]) => { + // setSelectedState(selectedItems); + }, + }} + sorting={{ + sort: sortState, + }} + /> + + + ); +}); +ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx new file mode 100644 index 0000000000000..a54296f65a382 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + deleteRules, + duplicateRules, + enableRules, +} from '../../../../containers/detection_engine/rules/api'; +import { Action } from './reducer'; +import { Rule } from '../../../../containers/detection_engine/rules/types'; + +export const editRuleAction = () => {}; + +export const runRuleAction = () => {}; + +export const duplicateRuleAction = async ( + rule: Rule, + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); + const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion }); + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); + dispatch({ type: 'updateRules', rules: duplicatedRule }); +}; + +export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { + dispatch({ type: 'setExportPayload', exportPayload: rules }); +}; + +export const deleteRulesAction = async ( + ids: string[], + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const deletedRules = await deleteRules({ ids, kbnVersion }); + dispatch({ type: 'deleteRules', rules: deletedRules }); +}; + +export const enableRulesAction = async ( + ids: string[], + enabled: boolean, + dispatch: React.Dispatch, + kbnVersion: string +) => { + try { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const updatedRules = await enableRules({ ids, enabled, kbnVersion }); + dispatch({ type: 'updateRules', rules: updatedRules }); + } catch { + // TODO Add error toast support to actions (and @throw jsdoc to api calls) + dispatch({ type: 'updateLoading', ids, isLoading: false }); + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx new file mode 100644 index 0000000000000..c8fb9d98fde6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import * as i18n from '../translations'; +import { TableData } from '../types'; +import { Action } from './reducer'; +import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; + +export const getBatchItems = ( + selectedState: TableData[], + dispatch: React.Dispatch, + closePopover: () => void, + kbnVersion: string +) => { + const containsEnabled = selectedState.some(v => v.activate); + const containsDisabled = selectedState.some(v => !v.activate); + const containsLoading = selectedState.some(v => v.isLoading); + + return [ + { + closePopover(); + const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); + await enableRulesAction(deactivatedIds, true, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_ACTIVATE_SELECTED} + , + { + closePopover(); + const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); + await enableRulesAction(activatedIds, false, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_DEACTIVATE_SELECTED} + , + { + closePopover(); + await exportRulesAction( + selectedState.map(s => s.sourceRule), + dispatch + ); + }} + > + {i18n.BATCH_ACTION_EXPORT_SELECTED} + , + { + closePopover(); + }} + > + {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} + , + { + closePopover(); + await deleteRulesAction( + selectedState.map(({ sourceRule: { id } }) => id), + dispatch, + kbnVersion + ); + }} + > + {i18n.BATCH_ACTION_DELETE_SELECTED} + , + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx new file mode 100644 index 0000000000000..cae0fb3eaf906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiHealth, EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { + deleteRulesAction, + duplicateRuleAction, + editRuleAction, + enableRulesAction, + exportRulesAction, + runRuleAction, +} from './actions'; + +import { Action } from './reducer'; +import { TableData } from '../types'; +import * as i18n from '../translations'; +import { PreferenceFormattedDate } from '../../../../components/formatted_date'; +import { RuleSwitch } from '../components/rule_switch'; + +const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + description: i18n.EDIT_RULE_SETTINGS, + icon: 'visControls', + name: i18n.EDIT_RULE_SETTINGS, + onClick: editRuleAction, + enabled: () => false, + }, + { + description: i18n.RUN_RULE_MANUALLY, + icon: 'play', + name: i18n.RUN_RULE_MANUALLY, + onClick: runRuleAction, + enabled: () => false, + }, + { + description: i18n.DUPLICATE_RULE, + icon: 'copy', + name: i18n.DUPLICATE_RULE, + onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch, kbnVersion), + }, + { + description: i18n.EXPORT_RULE, + icon: 'exportAction', + name: i18n.EXPORT_RULE, + onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), + }, + { + description: i18n.DELETE_RULE, + icon: 'trash', + name: i18n.DELETE_RULE, + onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, kbnVersion), + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const getColumns = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + field: 'rule', + name: i18n.COLUMN_RULE, + render: (value: TableData['rule']) => {value.name}, + truncateText: true, + width: '24%', + }, + { + field: 'method', + name: i18n.COLUMN_METHOD, + truncateText: true, + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: TableData['severity']) => ( + + {value} + + ), + truncateText: true, + }, + { + field: 'lastCompletedRun', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: TableData['lastCompletedRun']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + sortable: true, + truncateText: true, + width: '16%', + }, + { + field: 'lastResponse', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: TableData['lastResponse']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); + }, + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: TableData['tags']) => ( +
+ <> + {value.map((tag, i) => ( + + {tag} + + ))} + +
+ ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: TableData['activate'], item: TableData) => ( + { + await enableRulesAction([id], enabled, dispatch, kbnVersion); + }} + /> + ), + sortable: true, + width: '85px', + }, + { + actions: getActions(dispatch, kbnVersion), + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts new file mode 100644 index 0000000000000..db02d41771f68 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.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 { Rule } from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { getEmptyValue } from '../../../../components/empty_value'; + +export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] => + rules.map(rule => ({ + id: rule.id, + rule_id: rule.rule_id, + rule: { + href: `#/detection-engine/rules/rule-details/${encodeURIComponent(rule.id)}`, + name: rule.name, + status: 'Status Placeholder', + }, + method: rule.type, // TODO: Map to i18n? + severity: rule.severity, + lastCompletedRun: undefined, // TODO: Not available yet + lastResponse: { + type: getEmptyValue(), // TODO: Not available yet + }, + tags: rule.tags ?? [], + activate: rule.enabled, + sourceRule: rule, + isLoading: selectedIds?.includes(rule.id) ?? false, + })); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx new file mode 100644 index 0000000000000..a73ebeb61db3c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiBasicTable, + EuiContextMenuPanel, + EuiFieldSearch, + EuiLoadingContent, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; + +import uuid from 'uuid'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getColumns } from './columns'; +import { useRules } from '../../../../containers/detection_engine/rules/use_rules'; +import { Loader } from '../../../../components/loader'; +import { Panel } from '../../../../components/panel'; +import { getBatchItems } from './batch_actions'; +import { EuiBasicTableOnChange, TableData } from '../types'; +import { allRulesReducer, State } from './reducer'; +import * as i18n from '../translations'; +import { useKibanaUiSetting } from '../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; +import { JSONDownloader } from '../components/json_downloader'; +import { useStateToaster } from '../../../../components/toasters'; + +const initialState: State = { + isLoading: true, + rules: [], + tableData: [], + selectedItems: [], + refreshToggle: true, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, +}; + +/** + * Table Component for displaying all Rules for a given cluster. Provides the ability to filter + * by name, sort by enabled, and perform the following actions: + * * Enable/Disable + * * Duplicate + * * Delete + * * Import/Export + */ +export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, + }, + dispatch, + ] = useReducer(allRulesReducer, initialState); + + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch, kbnVersion] + ); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + + if (!isLoadingRules) { + setIsInitialLoad(false); + } + }, [isLoadingRules]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + dispatch({ + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, + }, + }); + }, [rulesData]); + + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + + + + {isInitialLoad ? ( + + ) : ( + <> + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + }} + /> + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + + {i18n.BATCH_ACTIONS} + + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: page.index + 1, perPage: page.size }, + }); + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + sortField: 'enabled', // Only enabled is supported for sorting currently + sortOrder: sort.direction, + }, + }); + }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + /> + {isLoading && } + + )} + + + ); +}); + +AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts new file mode 100644 index 0000000000000..c59c5687c10c9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterOptions, + PaginationOptions, + Rule, +} from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { formatRules } from './helpers'; + +export interface State { + isLoading: boolean; + rules: Rule[]; + selectedItems: TableData[]; + pagination: PaginationOptions; + filterOptions: FilterOptions; + refreshToggle: boolean; + tableData: TableData[]; + exportPayload?: object[]; +} + +export type Action = + | { type: 'refresh' } + | { type: 'loading'; isLoading: boolean } + | { type: 'deleteRules'; rules: Rule[] } + | { type: 'duplicate'; rule: Rule } + | { type: 'setExportPayload'; exportPayload?: object[] } + | { type: 'setSelected'; selectedItems: TableData[] } + | { type: 'updateLoading'; ids: string[]; isLoading: boolean } + | { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions } + | { type: 'updatePagination'; pagination: PaginationOptions } + | { type: 'updateFilterOptions'; filterOptions: FilterOptions } + | { type: 'failure' }; + +export const allRulesReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'refresh': { + return { + ...state, + refreshToggle: !state.refreshToggle, + }; + } + case 'updateRules': { + // If pagination included, this was a hard refresh + if (action.pagination) { + return { + ...state, + rules: action.rules, + pagination: action.pagination, + tableData: formatRules(action.rules), + }; + } + + const ruleIds = state.rules.map(r => r.rule_id); + const updatedRules = action.rules.reduce( + (rules, updatedRule) => + ruleIds.includes(updatedRule.rule_id) + ? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)) + : [...rules, updatedRule], + [...state.rules] + ); + + // Update enabled on selectedItems so that batch actions show correct available actions + const updatedRuleIdToState = action.rules.reduce>( + (acc, r) => ({ ...acc, [r.id]: r.enabled }), + {} + ); + const updatedSelectedItems = state.selectedItems.map(selectedItem => + Object.keys(updatedRuleIdToState).includes(selectedItem.id) + ? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] } + : selectedItem + ); + + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + selectedItems: updatedSelectedItems, + }; + } + case 'updatePagination': { + return { + ...state, + pagination: action.pagination, + }; + } + case 'updateFilterOptions': { + return { + ...state, + filterOptions: action.filterOptions, + }; + } + case 'deleteRules': { + const deletedRuleIds = action.rules.map(r => r.rule_id); + const updatedRules = state.rules.reduce( + (rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]), + [] + ); + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + }; + } + case 'setSelected': { + return { + ...state, + selectedItems: action.selectedItems, + }; + } + case 'updateLoading': { + return { + ...state, + rules: state.rules, + tableData: formatRules(state.rules, action.ids), + }; + } + case 'loading': { + return { + ...state, + isLoading: action.isLoading, + }; + } + case 'failure': { + return { + ...state, + isLoading: false, + rules: [], + }; + } + case 'setExportPayload': { + return { + ...state, + exportPayload: action.exportPayload, + }; + } + default: + return state; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..6b0aa02d4edfa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImportRuleModal renders correctly against snapshot 1`] = ` + + + + + + Import rule + + + + +

+ Select a SIEM rule (as exported from the Detection Engine UI) to import +

+
+ + + + +
+ + + Cancel + + + Import rule + + +
+
+
+`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx new file mode 100644 index 0000000000000..b397e50201f14 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx @@ -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 { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { ImportRuleModal } from './index'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { getMockKibanaUiSetting, MockFrameworks } from '../../../../../mock'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; + +const mockUseKibanaUiSetting: jest.Mock = useKibanaUiSetting as jest.Mock; +jest.mock('../../../../../lib/settings/use_kibana_ui_setting', () => ({ + useKibanaUiSetting: jest.fn(), +})); + +describe('ImportRuleModal', () => { + test('renders correctly against snapshot', () => { + mockUseKibanaUiSetting.mockImplementation( + getMockKibanaUiSetting((DEFAULT_KBN_VERSION as unknown) as MockFrameworks) + ); + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx new file mode 100644 index 0000000000000..fdcf6263f414f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCheckbox, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + // @ts-ignore no-exported-member + EuiFilePicker, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import React, { useCallback, useState } from 'react'; +import { failure } from 'io-ts/lib/PathReporter'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import * as i18n from './translations'; +import { duplicateRules } from '../../../../../containers/detection_engine/rules/api'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; +import { ndjsonToJSON } from '../json_downloader'; +import { RulesSchema } from '../../../../../containers/detection_engine/rules/types'; +import { useStateToaster } from '../../../../../components/toasters'; + +interface ImportRuleModalProps { + showModal: boolean; + closeModal: () => void; + importComplete: () => void; +} + +/** + * Modal component for importing Rules from a json file + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const ImportRuleModal = React.memo( + ({ showModal, closeModal, importComplete }) => { + const [selectedFiles, setSelectedFiles] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const cleanupAndCloseModal = () => { + setIsImporting(false); + setSelectedFiles(null); + closeModal(); + }; + + const importRules = useCallback(async () => { + if (selectedFiles != null) { + setIsImporting(true); + const reader = new FileReader(); + reader.onload = async event => { + // @ts-ignore type is string, not ArrayBuffer as FileReader.readAsText is called + const importedRules = ndjsonToJSON(event?.target?.result ?? ''); + + const decodedRules = pipe( + RulesSchema.decode(importedRules), + fold(errors => { + cleanupAndCloseModal(); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.IMPORT_FAILED, + color: 'danger', + iconType: 'alert', + errors: failure(errors), + }, + }); + throw new Error(failure(errors).join('\n')); + }, identity) + ); + + const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); + importComplete(); + cleanupAndCloseModal(); + + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_IMPORTED_RULES(duplicatedRules.length), + color: 'success', + iconType: 'check', + }, + }); + }; + Object.values(selectedFiles).map(f => reader.readAsText(f)); + } + }, [selectedFiles]); + + return ( + <> + {showModal && ( + + + + {i18n.IMPORT_RULE} + + + + +

{i18n.SELECT_RULE}

+
+ + + { + setSelectedFiles(Object.keys(files).length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {}} + /> +
+ + + {i18n.CANCEL_BUTTON} + + {i18n.IMPORT_RULE} + + +
+
+ )} + + ); + } +); + +ImportRuleModal.displayName = 'ImportRuleModal'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts new file mode 100644 index 0000000000000..50c3c75b6109f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const IMPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importRuleTitle', + { + defaultMessage: 'Import rule', + } +); + +export const SELECT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.selectRuleDescription', + { + defaultMessage: 'Select a SIEM rule (as exported from the Detection Engine UI) to import', + } +); + +export const INITIAL_PROMPT_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.initialPromptTextDescription', + { + defaultMessage: 'Select or drag and drop files', + } +); + +export const OVERWRITE_WITH_SAME_NAME = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.overwriteDescription', + { + defaultMessage: 'Automatically overwrite saved objects with the same name', + } +); + +export const CANCEL_BUTTON = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const SUCCESSFULLY_IMPORTED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully imported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + } + ); + +export const IMPORT_FAILED = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importFailedTitle', + { + defaultMessage: 'Failed to import rules', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..c4377c265c2c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSONDownloader renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx new file mode 100644 index 0000000000000..ef6493f89f383 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { JSONDownloader, jsonToNDJSON, ndjsonToJSON } from './index'; + +const jsonArray = [ + { + description: 'Detecting root and admin users1', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 100, + }, + { + description: 'Detecting root and admin users2', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 101, + }, +]; + +const ndjson = `{"description":"Detecting root and admin users1","created_by":"elastic","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"description":"Detecting root and admin users2","created_by":"elastic","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +const ndjsonSorted = `{"created_by":"elastic","description":"Detecting root and admin users1","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"created_by":"elastic","description":"Detecting root and admin users2","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +describe('JSONDownloader', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + describe('jsonToNDJSON', () => { + test('converts to NDJSON', () => { + const output = jsonToNDJSON(jsonArray, false); + expect(output).toEqual(ndjson); + }); + + test('converts to NDJSON with keys sorted', () => { + const output = jsonToNDJSON(jsonArray); + expect(output).toEqual(ndjsonSorted); + }); + }); + + describe('ndjsonToJSON', () => { + test('converts to JSON', () => { + const output = ndjsonToJSON(ndjson); + expect(output).toEqual(jsonArray); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx new file mode 100644 index 0000000000000..e9c2c69f067cc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; + +const InvisibleAnchor = styled.a` + display: none; +`; + +export interface JSONDownloaderProps { + filename: string; + payload?: object[]; + onExportComplete: (exportCount: number) => void; +} + +/** + * Component for downloading JSON as a file. Download will occur on each update to `payload` param + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const JSONDownloader = React.memo( + ({ filename, payload, onExportComplete }) => { + const anchorRef = useRef(null); + + useEffect(() => { + if (anchorRef && anchorRef.current && payload != null) { + const blob = new Blob([jsonToNDJSON(payload)], { type: 'application/json' }); + // @ts-ignore function is not always defined -- this is for supporting IE + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob); + } else { + const objectURL = window.URL.createObjectURL(blob); + anchorRef.current.href = objectURL; + anchorRef.current.download = filename; + anchorRef.current.click(); + window.URL.revokeObjectURL(objectURL); + } + onExportComplete(payload.length); + } + }, [payload]); + + return ; + } +); + +JSONDownloader.displayName = 'JSONDownloader'; + +export const jsonToNDJSON = (jsonArray: object[], sortKeys = true): string => { + return jsonArray + .map(j => JSON.stringify(j, sortKeys ? Object.keys(j).sort() : null, 0)) + .join('\n'); +}; + +export const ndjsonToJSON = (ndjson: string): object[] => { + const jsonLines = ndjson.split(/\r?\n/); + return jsonLines.reduce((acc, line) => { + try { + return [...acc, JSON.parse(line)]; + } catch (e) { + return acc; + } + }, []); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..98f8ae6a80e07 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleSwitch renders correctly against snapshot 1`] = ` + + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx new file mode 100644 index 0000000000000..9e5f4317678e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { RuleSwitch } from './index'; + +describe('RuleSwitch', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx new file mode 100644 index 0000000000000..da58b2e076e0d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; + +const StaticSwitch = styled(EuiSwitch)` + .euiSwitch__thumb, + .euiSwitch__icon { + transition: none; + } +`; + +StaticSwitch.displayName = 'StaticSwitch'; + +export interface RuleSwitchProps { + id: string; + enabled: boolean; + isLoading: boolean; + onRuleStateChange: (isEnabled: boolean, id: string) => void; +} + +/** + * Basic switch component for displaying loader when enabled/disabled + */ +export const RuleSwitch = React.memo( + ({ id, enabled, isLoading, onRuleStateChange }) => { + return ( + + + {isLoading ? ( + + ) : ( + { + onRuleStateChange(e.target.checked!, id); + }} + /> + )} + + + ); + } +); + +RuleSwitch.displayName = 'RuleSwitch'; 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 9d2f5dc0a6c29..afff0f07dfac4 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 @@ -4,1049 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { - EuiBadge, - EuiBasicTable, - EuiButton, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiIconTip, - EuiLink, - EuiPanel, - EuiSpacer, - EuiSwitch, - EuiTabbedContent, - EuiTextColor, -} from '@elastic/eui'; -import moment from 'moment'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui'; import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; -import { HeaderSection } from '../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../components/detection_engine/utility_bar'; + import { WrapperPage } from '../../../components/wrapper_page'; import { SpyRoute } from '../../../utils/route/spy_routes'; import * as i18n from './translations'; - -// Michael: Will need to change this to get the current datetime format from Kibana settings. -const dateTimeFormat = (value: string) => { - return moment(value).format('M/D/YYYY, h:mm A'); -}; - -const AllRules = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - status: string; - } - - interface LastResponseTypes { - type: string; - message?: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - method: string; - severity: string; - lastCompletedRun: string; - lastResponse: LastResponseTypes; - tags: string | string[]; - activate: boolean; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - description: 'Edit rule settings', - icon: 'visControls', - name: 'Edit rule settings', - onClick: () => {}, - }, - { - description: 'Run rule manually…', - icon: 'play', - name: 'Run rule manually…', - onClick: () => {}, - }, - { - description: 'Duplicate rule…', - icon: 'copy', - name: 'Duplicate rule…', - onClick: () => {}, - }, - { - description: 'Export rule', - icon: 'exportAction', - name: 'Export rule', - onClick: () => {}, - }, - { - description: 'Delete rule…', - icon: 'trash', - name: 'Delete rule…', - onClick: () => {}, - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => ( -
- {value.name}{' '} - {value.status} -
- ), - sortable: true, - truncateText: true, - width: '24%', - }, - { - field: 'method', - name: 'Method', - sortable: true, - truncateText: true, - }, - { - field: 'severity', - name: 'Severity', - render: (value: ColumnTypes['severity']) => ( - - {value} - - ), - sortable: true, - truncateText: true, - }, - { - field: 'lastCompletedRun', - name: 'Last completed run', - render: (value: ColumnTypes['lastCompletedRun']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - - ); - }, - sortable: true, - truncateText: true, - width: '16%', - }, - { - field: 'lastResponse', - name: 'Last response', - render: (value: ColumnTypes['lastResponse']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - field: 'tags', - name: 'Tags', - render: (value: ColumnTypes['tags']) => ( -
- {typeof value !== 'string' ? ( - <> - {value.map((tag, i) => ( - - {tag} - - ))} - - ) : ( - {value} - )} -
- ), - sortable: true, - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'activate', - name: 'Activate', - render: (value: ColumnTypes['activate']) => ( - {}} showLabel={false} /> - ), - sortable: true, - width: '65px', - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Low', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: ['attack.t1234', 'attack.t4321'], - activate: true, - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Medium', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Fail', - message: 'Full fail message here.', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'High', - tags: 'attack.t1234', - activate: false, - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'rule', direction: 'asc' }); - - return ( - <> - - - - - - - - - - - {'Showing: 39 rules'} - - - - {'Selected: 2 rules'} - - {'Batch actions context menu here.'}

} - > - {'Batch actions'} -
-
- - - {'Clear 7 filters'} - -
-
- - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: () => true, - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> -
- - ); -}); -AllRules.displayName = 'AllRules'; - -const ActivityMonitor = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - ran: string; - lookedBackTo: string; - status: string; - response: string | undefined; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => {value.name}, - sortable: true, - truncateText: true, - }, - { - field: 'ran', - name: 'Ran', - render: (value: ColumnTypes['ran']) => , - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo', - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo']) => ( - - ), - sortable: true, - truncateText: true, - }, - { - field: 'status', - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response', - name: 'Response', - render: (value: ColumnTypes['response']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - return ( - <> - - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: (item: ColumnTypes) => item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? undefined : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; +import { AllRules } from './all_rules'; +import { ActivityMonitor } from './activity_monitor'; +import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../components/empty_value'; +import { ImportRuleModal } from './components/import_rule_modal'; export const RulesComponent = React.memo(() => { + const [showImportModal, setShowImportModal] = useState(false); + const [importCompleteToggle, setImportCompleteToggle] = useState(false); + + const lastCompletedRun = undefined; return ( <> + setShowImportModal(false)} + importComplete={() => setImportCompleteToggle(!importCompleteToggle)} + /> , + }} + /> + ) : ( + getEmptyTagValue() + ) + } title={i18n.PAGE_TITLE} > - - {'Import rule…'} + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} - {'Add new rule'} + {i18n.ADD_NEW_RULE} @@ -1056,12 +73,12 @@ export const RulesComponent = React.memo(() => { tabs={[ { id: 'tabAllRules', - name: 'All rules', - content: , + name: i18n.ALL_RULES, + content: , }, { id: 'tabActivityMonitor', - name: 'Activity monitor', + name: i18n.ACTIVITY_MONITOR, content: , }, ]} @@ -1072,4 +89,5 @@ export const RulesComponent = React.memo(() => { ); }); + RulesComponent.displayName = 'RulesComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 2b20c726d4b3f..9ae266e396f6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -6,6 +6,202 @@ import { i18n } from '@kbn/i18n'; +export const BACK_TO_DETECTION_ENGINE = i18n.translate( + 'xpack.siem.detectionEngine.rules.backOptionsHeader', + { + defaultMessage: 'Back to detection engine', + } +); + +export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.importRuleTitle', { + defaultMessage: 'Import rule…', +}); + +export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', { + defaultMessage: 'Add new rule', +}); + +export const ACTIVITY_MONITOR = i18n.translate( + 'xpack.siem.detectionEngine.rules.activityMonitorTitle', + { + defaultMessage: 'Activity monitor', + } +); + export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { defaultMessage: 'Rules', }); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { + defaultMessage: 'Refresh', +}); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle', + { + defaultMessage: 'Batch actions', + } +); + +export const BATCH_ACTION_ACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.activateSelectedTitle', + { + defaultMessage: 'Activate selected', + } +); + +export const BATCH_ACTION_DEACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle', + { + defaultMessage: 'Deactivate selected', + } +); + +export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.exportSelectedTitle', + { + defaultMessage: 'Export selected', + } +); + +export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle', + { + defaultMessage: 'Edit selected index patterns…', + } +); + +export const BATCH_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected…', + } +); + +export const EXPORT_FILENAME = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.exportFilenameTitle', + { + defaultMessage: 'rules_export', + } +); + +export const SUCCESSFULLY_EXPORTED_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.successfullyExportedRulesTitle', { + values: { totalRules }, + defaultMessage: + 'Successfully exported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const ALL_RULES = i18n.translate('xpack.siem.detectionEngine.rules.allRules.tableTitle', { + defaultMessage: 'All rules', +}); + +export const SEARCH_RULES = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchAriaLabel', + { + defaultMessage: 'Search rules', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchPlaceholder', + { + defaultMessage: 'e.g. rule name', + } +); + +export const SHOWING_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.showingRulesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const SELECTED_RULES = (selectedRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.selectedRulesTitle', { + values: { selectedRules }, + defaultMessage: 'Selected {selectedRules} {selectedRules, plural, =1 {rule} other {rules}}', + }); + +export const EDIT_RULE_SETTINGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.editRuleSettingsDescription', + { + defaultMessage: 'Edit rule settings', + } +); + +export const RUN_RULE_MANUALLY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.runRuleManuallyDescription', + { + defaultMessage: 'Run rule manually…', + } +); + +export const DUPLICATE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.duplicateRuleDescription', + { + defaultMessage: 'Duplicate rule…', + } +); + +export const EXPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.exportRuleDescription', + { + defaultMessage: 'Export rule', + } +); + +export const DELETE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.deleteeRuleDescription', + { + defaultMessage: 'Delete rule…', + } +); + +export const COLUMN_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const COLUMN_METHOD = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const COLUMN_SEVERITY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastCompletedRunTitle', + { + defaultMessage: 'Last completed run', + } +); + +export const COLUMN_LAST_RESPONSE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastResponseTitle', + { + defaultMessage: 'Last response', + } +); + +export const COLUMN_TAGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.tagsTitle', + { + defaultMessage: 'Tags', + } +); + +export const COLUMN_ACTIVATE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle', + { + defaultMessage: 'Activate', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..8cbc61e677f8c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Rule } from '../../../containers/detection_engine/rules/types'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort: EuiBasicTableSortTypes; +} + +export interface TableData { + id: string; + rule_id: string; + rule: { + href: string; + name: string; + status: string; + }; + method: string; + severity: string; + lastCompletedRun: string | undefined; + lastResponse: { + type: string; + message?: string; + }; + tags: string[]; + activate: boolean; + isLoading: boolean; + sourceRule: Rule; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index eb816876bdba8..2cc98930767dc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -59,7 +59,7 @@ const calculateFlyoutHeight = ({ export const HomePage = pure(() => ( {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( - +
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx index 48b6d34d0b28b..1252c7031e8a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -10,6 +10,8 @@ import { Route, Switch } from 'react-router-dom'; import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; import { Anomaly } from '../../../components/ml/types'; import { HostsTableType } from '../../../store/hosts/model'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; import { HostDetailsTabsProps } from './types'; import { type } from './utils'; @@ -18,7 +20,6 @@ import { HostsQueryTabBody, AuthenticationsQueryTabBody, UncommonProcessQueryTabBody, - AnomaliesQueryTabBody, EventsQueryTabBody, } from '../navigation'; @@ -84,7 +85,9 @@ const HostDetailsTabs = React.memo( /> } + render={() => ( + + )} /> ( /> } + render={() => ( + + )} /> ( - -); - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts index 8a8f23208363d..f20138f520620 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './anomalies_query_tab_body'; export * from './authentications_query_tab_body'; export * from './events_query_tab_body'; export * from './hosts_query_tab_body'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts index d567038a05bd8..98d931dd7e275 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts @@ -25,6 +25,18 @@ type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPe export type HostsNavTab = Record; +export type SetQuery = ({ + id, + inspect, + loading, + refetch, +}: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; +}) => void; + interface QueryTabBodyProps { type: hostsModel.HostsType; startDate: number; @@ -32,30 +44,13 @@ interface QueryTabBodyProps { filterQuery?: string | ESTermQuery; } -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { - skip: boolean; - narrowDateRange: NarrowDateRange; - hostName?: string; -}; - export type HostsComponentsQueryProps = QueryTabBodyProps & { deleteQuery?: ({ id }: { id: string }) => void; indexPattern: StaticIndexPattern; skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; + setQuery: SetQuery; updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; }; export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; -export type AnomaliesChildren = (args: AnomaliesQueryTabBodyProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx index 96111f0479938..477f435b84b20 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx @@ -39,6 +39,7 @@ import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; import { TlsQueryTable } from './tls_query_table'; import { IPDetailsComponentProps } from './types'; import { UsersQueryTable } from './users_query_table'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; import { esQuery } from '../../../../../../../../src/plugins/data/public'; export { getBreadcrumbs } from './utils'; @@ -58,6 +59,7 @@ export const IPDetailsComponent = React.memo( setQuery, to, }) => { + const type = networkModel.NetworkType.details; const narrowDateRange = useCallback( (score, interval) => { const fromTo = scoreIntervalToDateTime(score, interval); @@ -108,7 +110,7 @@ export const IPDetailsComponent = React.memo( skip={isInitializing} sourceId="default" filterQuery={filterQuery} - type={networkModel.NetworkType.details} + type={type} ip={ip} > {({ id, inspect, ipOverviewData, loading, refetch }) => ( @@ -127,7 +129,7 @@ export const IPDetailsComponent = React.memo( anomaliesData={anomaliesData} loading={loading} isLoadingAnomaliesData={isLoadingAnomaliesData} - type={networkModel.NetworkType.details} + type={type} flowTarget={flowTarget} refetch={refetch} setQuery={setQuery} @@ -158,7 +160,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -172,7 +174,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -190,7 +192,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -204,7 +206,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -220,7 +222,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} /> @@ -232,7 +234,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} /> @@ -246,19 +248,23 @@ export const IPDetailsComponent = React.memo( setQuery={setQuery} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} /> - diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx deleted file mode 100644 index daf9cd2dd1d12..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx +++ /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 React from 'react'; -import { AnomaliesQueryTabBodyProps } from './types'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; - -export const AnomaliesQueryTabBody = ({ - to, - isInitializing, - from, - type, - narrowDateRange, -}: AnomaliesQueryTabBodyProps) => ( - -); - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx index 80084de743526..0fcd7fa48f73c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx @@ -5,14 +5,12 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export const ConditionalFlexGroup = styled(EuiFlexGroup)` - ${() => css` - @media only screen and (min-width: 1441px) { - flex-direction: row; - } - `} + @media only screen and (min-width: 1441px) { + flex-direction: row; + } `; ConditionalFlexGroup.displayName = 'ConditionalFlexGroup'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx index 0fe370c144049..6ddd3bbec3a32 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx @@ -17,21 +17,21 @@ import { IPsQueryTabBodyProps as CountriesQueryTabBodyProps } from './types'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); export const CountriesQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, indexPattern, flowTarget, }: CountriesQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx index 589c3b5f53533..da3c2fcfbc67b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx @@ -7,57 +7,88 @@ import React from 'react'; import { getOr } from 'lodash/fp'; +import { EuiSpacer } from '@elastic/eui'; import { NetworkDnsTable } from '../../../components/page/network/network_dns_table'; -import { NetworkDnsQuery } from '../../../containers/network_dns'; +import { NetworkDnsQuery, NetworkDnsHistogramQuery } from '../../../containers/network_dns'; import { manageQuery } from '../../../components/page/manage_query'; -import { DnsQueryTabBodyProps } from './types'; +import { NetworkComponentQueryProps } from './types'; +import { NetworkDnsHistogram } from '../../../components/page/network/dns_histogram'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); +const NetworkDnsHistogramManage = manageQuery(NetworkDnsHistogram); export const DnsQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, type, -}: DnsQueryTabBodyProps) => ( - - {({ - totalCount, - loading, - networkDns, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + updateDateRange = () => {}, +}: NetworkComponentQueryProps) => ( + <> + + {({ totalCount, loading, id, inspect, refetch, histogram }) => ( + + )} + + + + {({ + totalCount, + loading, + networkDns, + pageInfo, + loadPage, + id, + inspect, + isInspected, + refetch, + histogram, + }) => ( + + )} + + ); DnsQueryTabBody.displayName = 'DNSQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx index a20a212623fb8..639a14d354ced 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx @@ -17,18 +17,18 @@ import { HttpQueryTabBodyProps } from './types'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); export const HttpQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, }: HttpQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts index 9e8b4c6215031..44b78cb3077ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './anomalies_query_tab_body'; export * from './network_routes'; export * from './network_routes_loading'; export * from './nav_tabs'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx index 08ba75443b333..95aaa90fe7865 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx @@ -17,21 +17,21 @@ import { IPsQueryTabBodyProps } from './types'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); export const IPsQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, indexPattern, flowTarget, }: IPsQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 955670b4b098d..681e1f8e1e34d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -14,11 +14,13 @@ import { scoreIntervalToDateTime } from '../../../components/ml/score/score_inte import { IPsQueryTabBody } from './ips_query_tab_body'; import { CountriesQueryTabBody } from './countries_query_tab_body'; import { HttpQueryTabBody } from './http_query_tab_body'; -import { AnomaliesQueryTabBody } from './anomalies_query_tab_body'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; import { TlsQueryTabBody } from './tls_query_tab_body'; +import { Anomaly } from '../../../components/ml/types'; export const NetworkRoutes = ({ networkPagePath, @@ -32,7 +34,7 @@ export const NetworkRoutes = ({ setAbsoluteRangeDatePicker, }: NetworkRoutesProps) => { const narrowDateRange = useCallback( - (score, interval) => { + (score: Anomaly, interval: string) => { const fromTo = scoreIntervalToDateTime(score, interval); setAbsoluteRangeDatePicker({ id: 'global', @@ -42,24 +44,51 @@ export const NetworkRoutes = ({ }, [scoreIntervalToDateTime, setAbsoluteRangeDatePicker] ); + const updateDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [from, to] + ); - const tabProps = { - networkPagePath, + const networkAnomaliesFilterQuery = { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }; + + const commonProps = { + startDate: from, + endDate: to, + skip: isInitializing, type, - to, + narrowDateRange, + setQuery, filterQuery, - isInitializing, - from, + }; + + const tabProps = { + ...commonProps, indexPattern, - setQuery, + updateDateRange, }; const anomaliesProps = { - from, - to, - isInitializing, - type, - narrowDateRange, + ...commonProps, + anomaliesFilterQuery: networkAnomaliesFilterQuery, + AnomaliesTableComponent: AnomaliesNetworkTable, }; return ( @@ -107,7 +136,12 @@ export const NetworkRoutes = ({ /> } + render={() => ( + + )} /> ); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx index 1f93e293be865..0adfec203e0a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx @@ -13,23 +13,23 @@ import { TlsQueryTabBodyProps } from './types'; const TlsTableManage = manageQuery(TlsTable); export const TlsQueryTabBody = ({ - to, + endDate, filterQuery, flowTarget, ip = '', setQuery, - isInitializing, - from, + skip, + startDate, type, }: TlsQueryTabBodyProps) => ( {({ id, inspect, isInspected, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index 9682495b6f66a..bc63e26f71eba 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -10,38 +10,37 @@ import { NavTab } from '../../../components/navigation/types'; import { FlowTargetSourceDest } from '../../../graphql/types'; import { networkModel } from '../../../store'; import { ESTermQuery } from '../../../../common/typed_json'; -import { NarrowDateRange } from '../../../components/ml/types'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { NarrowDateRange } from '../../../components/ml/types'; -interface QueryTabBodyProps { +interface QueryTabBodyProps extends Pick { + skip: boolean; type: networkModel.NetworkType; + startDate: number; + endDate: number; filterQuery?: string | ESTermQuery; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; } -export type DnsQueryTabBodyProps = QueryTabBodyProps & GlobalTimeArgs; +export type NetworkComponentQueryProps = QueryTabBodyProps; -export type IPsQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - indexPattern: StaticIndexPattern; - flowTarget: FlowTargetSourceDest; - }; +export type IPsQueryTabBodyProps = QueryTabBodyProps & { + indexPattern: StaticIndexPattern; + flowTarget: FlowTargetSourceDest; +}; -export type TlsQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - flowTarget: FlowTargetSourceDest; - ip?: string; - }; +export type TlsQueryTabBodyProps = QueryTabBodyProps & { + flowTarget: FlowTargetSourceDest; + ip?: string; +}; -export type HttpQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - ip?: string; - }; -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & - Pick & { - narrowDateRange: NarrowDateRange; - }; +export type HttpQueryTabBodyProps = QueryTabBodyProps & { + ip?: string; +}; export type NetworkRoutesProps = GlobalTimeArgs & { networkPagePath: string; diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js new file mode 100644 index 0000000000000..3e1c5f51ebb5c --- /dev/null +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../../src/setup_node_env'); + +const fs = require('fs'); +const path = require('path'); + +/* + * This script is used to parse a set of saved searches on a file system + * and output rule data compatible json files. + * Example: + * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules + * + * After editing any changes in the files of ${HOME}/saved_rules/*.json + * you can then post the rules with a CURL post script such as: + * + * ./post_rule.sh ${HOME}/saved_rules/*.json + * + * Note: This script is recursive and but does not preserve folder structure + * when it outputs the saved rules. + */ + +// Defaults of the outputted rules since the saved KQL searches do not have +// this type of information. You usually will want to make any hand edits after +// doing a search to KQL conversion before posting it as a rule or checking it +// into another repository. +const INTERVAL = '5m'; +const SEVERITY = 'low'; +const TYPE = 'query'; +const FROM = 'now-6m'; +const TO = 'now'; +const IMMUTABLE = true; +const RISK_SCORE = 50; +const ENABLED = false; +let allRules = ''; +const allRulesNdJson = 'all_rules.ndjson'; + +// For converting, if you want to use these instead of rely on the defaults then +// comment these in and use them for the script. Otherwise this is commented out +// so we can utilize the defaults of input and output which are based on saved objects +// of siem:defaultIndex and siem:defaultSignalsIndex +// const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; +// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; + +const walk = dir => { + const list = fs.readdirSync(dir); + return list.reduce((accum, file) => { + const fileWithDir = dir + '/' + file; + const stat = fs.statSync(fileWithDir); + if (stat && stat.isDirectory()) { + return [...accum, ...walk(fileWithDir)]; + } else { + return [...accum, fileWithDir]; + } + }, []); +}; + +//clean up the file system characters +const cleanupFileName = file => { + return path + .basename(file, path.extname(file)) + .replace(/\s+/g, '_') + .replace(/,/g, '') + .replace(/\+s/g, '') + .replace(/-/g, '') + .replace(/__/g, '_') + .toLowerCase(); +}; + +async function main() { + if (process.argv.length !== 4) { + throw new Error( + 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' + ); + } + + const files = process.argv[2]; + const outputDir = process.argv[3]; + + const savedSearchesJson = walk(files).filter(file => { + return !path.basename(file).startsWith('.') && file.endsWith('.ndjson'); + }); + + const savedSearchesParsed = savedSearchesJson.reduce((accum, json) => { + const jsonFile = fs.readFileSync(json, 'utf8'); + const jsonLines = jsonFile.split(/\r{0,1}\n/); + const parsedLines = jsonLines.reduce((accum, line, index) => { + try { + const parsedLine = JSON.parse(line); + if (index !== 0) { + parsedLine._file = `${json.substring(0, json.length - '.ndjson'.length)}_${String( + index + )}.ndjson`; + } else { + parsedLine._file = json; + } + parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( + parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + return [...accum, parsedLine]; + } catch (err) { + console.log('error parsing a line in this file:', json); + return accum; + } + }, []); + return [...accum, ...parsedLines]; + }, []); + + savedSearchesParsed.forEach( + ({ + _file, + attributes: { + description, + title, + kibanaSavedObjectMeta: { + searchSourceJSON: { + query: { query, language }, + filter, + }, + }, + }, + }) => { + const fileToWrite = cleanupFileName(_file); + + if (query != null && query.trim() !== '') { + const outputMessage = { + rule_id: fileToWrite, + risk_score: RISK_SCORE, + description: description || title, + immutable: IMMUTABLE, + interval: INTERVAL, + name: title, + severity: SEVERITY, + type: TYPE, + from: FROM, + to: TO, + query, + language, + filters: filter, + enabled: ENABLED, + // comment these in if you want to use these for input output, otherwise + // with these two commented out, we will use the default saved objects from spaces. + // index: INDEX, + // output_index: OUTPUT_INDEX, + }; + + fs.writeFileSync( + `${outputDir}/${fileToWrite}.json`, + JSON.stringify(outputMessage, null, 2) + ); + allRules += `${JSON.stringify(outputMessage)}\n`; + } + } + ); + fs.writeFileSync(`${outputDir}/${allRulesNdJson}`, allRules); +} + +if (require.main === module) { + main(); +} diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js deleted file mode 100644 index a1889a400a183..0000000000000 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ /dev/null @@ -1,147 +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. - */ - -require('../../../../../src/setup_node_env'); - -const fs = require('fs'); -const path = require('path'); - -/* - * This script is used to parse a set of saved searches on a file system - * and output signal data compatible json files. - * Example: - * node saved_query_to_signals.js ${HOME}/saved_searches ${HOME}/saved_signals - * - * After editing any changes in the files of ${HOME}/saved_signals/*.json - * you can then post the signals with a CURL post script such as: - * - * ./post_signal.sh ${HOME}/saved_signals/*.json - * - * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved signals. - */ - -// Defaults of the outputted signals since the saved KQL searches do not have -// this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a signal or checking it -// into another repository. -const INTERVAL = '5m'; -const SEVERITY = 'low'; -const TYPE = 'query'; -const FROM = 'now-6m'; -const TO = 'now'; -const IMMUTABLE = true; -const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; - -const walk = dir => { - const list = fs.readdirSync(dir); - return list.reduce((accum, file) => { - const fileWithDir = dir + '/' + file; - const stat = fs.statSync(fileWithDir); - if (stat && stat.isDirectory()) { - return [...accum, ...walk(fileWithDir)]; - } else { - return [...accum, fileWithDir]; - } - }, []); -}; - -//clean up the file system characters -const cleanupFileName = file => { - return path - .basename(file, path.extname(file)) - .replace(/\s+/g, '_') - .replace(/,/g, '') - .replace(/\+s/g, '') - .replace(/-/g, '') - .replace(/__/g, '_') - .toLowerCase(); -}; - -async function main() { - if (process.argv.length !== 4) { - throw new Error( - 'usage: saved_query_to_signals [input directory with saved searches] [output directory]' - ); - } - - const files = process.argv[2]; - const outputDir = process.argv[3]; - - const savedSearchesJson = walk(files).filter(file => { - return !path.basename(file).startsWith('.') && file.endsWith('.ndjson'); - }); - - const savedSearchesParsed = savedSearchesJson.reduce((accum, json) => { - const jsonFile = fs.readFileSync(json, 'utf8'); - const jsonLines = jsonFile.split(/\r{0,1}\n/); - const parsedLines = jsonLines.reduce((accum, line, index) => { - try { - const parsedLine = JSON.parse(line); - if (index !== 0) { - parsedLine._file = `${json.substring(0, json.length - '.ndjson'.length)}_${String( - index - )}.ndjson`; - } else { - parsedLine._file = json; - } - parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( - parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON - ); - return [...accum, parsedLine]; - } catch (err) { - console.log('error parsing a line in this file:', json); - return accum; - } - }, []); - return [...accum, ...parsedLines]; - }, []); - - savedSearchesParsed.forEach( - ({ - _file, - attributes: { - description, - title, - kibanaSavedObjectMeta: { - searchSourceJSON: { - query: { query, language }, - filter, - }, - }, - }, - }) => { - const fileToWrite = cleanupFileName(_file); - - if (query != null && query.trim() !== '') { - const outputMessage = { - rule_id: fileToWrite, - description: description || title, - immutable: IMMUTABLE, - index: INDEX, - interval: INTERVAL, - name: title, - severity: SEVERITY, - type: TYPE, - from: FROM, - to: TO, - query, - language, - filters: filter, - }; - - fs.writeFileSync( - `${outputDir}/${fileToWrite}.json`, - JSON.stringify(outputMessage, null, 2) - ); - } - } - ); -} - -if (require.main === module) { - main(); -} diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts new file mode 100644 index 0000000000000..4bfd6be173105 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createAnomaliesResolvers } from './resolvers'; +export { anomaliesSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts new file mode 100644 index 0000000000000..47e227a8c0f84 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Anomalies } from '../../lib/anomalies'; +import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; +import { createOptions } from '../../utils/build_query/create_options'; +import { QuerySourceResolver } from '../sources/resolvers'; +import { SourceResolvers } from '../types'; + +export interface AnomaliesResolversDeps { + anomalies: Anomalies; +} + +type QueryAnomaliesOverTimeResolver = ChildResolverOf< + AppResolverOf, + QuerySourceResolver +>; + +export const createAnomaliesResolvers = ( + libs: AnomaliesResolversDeps +): { + Source: { + AnomaliesOverTime: QueryAnomaliesOverTimeResolver; + }; +} => ({ + Source: { + async AnomaliesOverTime(source, args, { req }, info) { + const options = { + ...createOptions(source, args, info), + defaultIndex: args.defaultIndex, + }; + return libs.anomalies.getAnomaliesOverTime(req, options); + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts new file mode 100644 index 0000000000000..1dad0aafd55b0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const anomaliesSchema = gql` + type AnomaliesOverTimeData { + inspect: Inspect + anomaliesOverTime: [MatrixOverTimeHistogramData!]! + totalCount: Float! + } + + extend type Source { + AnomaliesOverTime( + timerange: TimerangeInput! + filterQuery: String + defaultIndex: [String!]! + ): AnomaliesOverTimeData! + } +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 110a390c19531..901d27295479a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -7,6 +7,7 @@ import { rootSchema } from '../../common/graphql/root'; import { sharedSchema } from '../../common/graphql/shared'; +import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; @@ -29,6 +30,7 @@ import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; export const schemas = [ + anomaliesSchema, authenticationsSchema, ecsSchema, eventsSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts index 84f8d004198e9..11aea7ffb07cf 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts @@ -145,11 +145,18 @@ export const networkSchema = gql` cursor: CursorType! } + type MatrixOverOrdinalHistogramData { + x: String! + y: Float! + g: String! + } + type NetworkDnsData { edges: [NetworkDnsEdges!]! totalCount: Float! pageInfo: PageInfoPaginated! inspect: Inspect + histogram: [MatrixOverOrdinalHistogramData!] } enum NetworkHttpFields { diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index d6a4d204124a1..fda79ad543bf6 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -458,6 +458,8 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; + + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -558,6 +560,28 @@ export interface IndexField { format?: Maybe; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface MatrixOverTimeHistogramData { + x: number; + + y: number; + + g: string; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -692,12 +716,6 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface Inspect { - dsl: string[]; - - response: string[]; -} - export interface AuthenticationsOverTimeData { inspect?: Maybe; @@ -706,14 +724,6 @@ export interface AuthenticationsOverTimeData { totalCount: number; } -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - export interface TimelineData { edges: TimelineEdges[]; @@ -1628,6 +1638,8 @@ export interface NetworkDnsData { pageInfo: PageInfoPaginated; inspect?: Maybe; + + histogram?: Maybe; } export interface NetworkDnsEdges { @@ -1650,6 +1662,14 @@ export interface NetworkDnsItem { uniqueDomains?: Maybe; } +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + export interface NetworkHttpData { edges: NetworkHttpEdges[]; @@ -2119,6 +2139,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AnomaliesOverTimeSourceArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; +} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2753,6 +2780,8 @@ export namespace SourceResolvers { configuration?: ConfigurationResolver; /** The status of the source */ status?: StatusResolver; + + AnomaliesOverTime?: AnomaliesOverTimeResolver; /** Gets Authentication success and failures based on a timerange */ Authentications?: AuthenticationsResolver; @@ -2824,6 +2853,19 @@ export namespace SourceResolvers { Parent, TContext >; + export type AnomaliesOverTimeResolver< + R = AnomaliesOverTimeData, + Parent = Source, + TContext = SiemContext + > = Resolver; + export interface AnomaliesOverTimeArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; + } + export type AuthenticationsResolver< R = AuthenticationsData, Parent = Source, @@ -3365,6 +3407,81 @@ export namespace IndexFieldResolvers { > = Resolver; } +export namespace AnomaliesOverTimeDataResolvers { + export interface Resolvers { + inspect?: InspectResolver, TypeParent, TContext>; + + anomaliesOverTime?: AnomaliesOverTimeResolver< + MatrixOverTimeHistogramData[], + TypeParent, + TContext + >; + + totalCount?: TotalCountResolver; + } + + export type InspectResolver< + R = Maybe, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type AnomaliesOverTimeResolver< + R = MatrixOverTimeHistogramData[], + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type TotalCountResolver< + R = number, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; +} + +export namespace InspectResolvers { + export interface Resolvers { + dsl?: DslResolver; + + response?: ResponseResolver; + } + + export type DslResolver = Resolver< + R, + Parent, + TContext + >; + export type ResponseResolver = Resolver< + R, + Parent, + TContext + >; +} + +export namespace MatrixOverTimeHistogramDataResolvers { + export interface Resolvers { + x?: XResolver; + + y?: YResolver; + + g?: GResolver; + } + + export type XResolver< + R = number, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; + export type YResolver< + R = number, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; + export type GResolver< + R = string, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; +} + export namespace AuthenticationsDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -3810,25 +3927,6 @@ export namespace PageInfoPaginatedResolvers { > = Resolver; } -export namespace InspectResolvers { - export interface Resolvers { - dsl?: DslResolver; - - response?: ResponseResolver; - } - - export type DslResolver = Resolver< - R, - Parent, - TContext - >; - export type ResponseResolver = Resolver< - R, - Parent, - TContext - >; -} - export namespace AuthenticationsOverTimeDataResolvers { export interface Resolvers { inspect?: InspectResolver, TypeParent, TContext>; @@ -3859,32 +3957,6 @@ export namespace AuthenticationsOverTimeDataResolvers { > = Resolver; } -export namespace MatrixOverTimeHistogramDataResolvers { - export interface Resolvers { - x?: XResolver; - - y?: YResolver; - - g?: GResolver; - } - - export type XResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; - export type YResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; - export type GResolver< - R = string, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; -} - export namespace TimelineDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -6949,6 +7021,8 @@ export namespace NetworkDnsDataResolvers { pageInfo?: PageInfoResolver; inspect?: InspectResolver, TypeParent, TContext>; + + histogram?: HistogramResolver, TypeParent, TContext>; } export type EdgesResolver< @@ -6971,6 +7045,11 @@ export namespace NetworkDnsDataResolvers { Parent = NetworkDnsData, TContext = SiemContext > = Resolver; + export type HistogramResolver< + R = Maybe, + Parent = NetworkDnsData, + TContext = SiemContext + > = Resolver; } export namespace NetworkDnsEdgesResolvers { @@ -7039,6 +7118,32 @@ export namespace NetworkDnsItemResolvers { > = Resolver; } +export namespace MatrixOverOrdinalHistogramDataResolvers { + export interface Resolvers { + x?: XResolver; + + y?: YResolver; + + g?: GResolver; + } + + export type XResolver< + R = string, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; + export type YResolver< + R = number, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; + export type GResolver< + R = string, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; +} + export namespace NetworkHttpDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -8602,6 +8707,9 @@ export type IResolvers = { SourceFields?: SourceFieldsResolvers.Resolvers; SourceStatus?: SourceStatusResolvers.Resolvers; IndexField?: IndexFieldResolvers.Resolvers; + AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers; + Inspect?: InspectResolvers.Resolvers; + MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers; AuthenticationItem?: AuthenticationItemResolvers.Resolvers; @@ -8614,9 +8722,7 @@ export type IResolvers = { OsEcsFields?: OsEcsFieldsResolvers.Resolvers; CursorType?: CursorTypeResolvers.Resolvers; PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers; - Inspect?: InspectResolvers.Resolvers; AuthenticationsOverTimeData?: AuthenticationsOverTimeDataResolvers.Resolvers; - MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers; TimelineData?: TimelineDataResolvers.Resolvers; TimelineEdges?: TimelineEdgesResolvers.Resolvers; TimelineItem?: TimelineItemResolvers.Resolvers; @@ -8704,6 +8810,7 @@ export type IResolvers = { NetworkDnsData?: NetworkDnsDataResolvers.Resolvers; NetworkDnsEdges?: NetworkDnsEdgesResolvers.Resolvers; NetworkDnsItem?: NetworkDnsItemResolvers.Resolvers; + MatrixOverOrdinalHistogramData?: MatrixOverOrdinalHistogramDataResolvers.Resolvers; NetworkHttpData?: NetworkHttpDataResolvers.Resolvers; NetworkHttpEdges?: NetworkHttpEdgesResolvers.Resolvers; NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index b040b773c1e53..08c481164d539 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -6,6 +6,7 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; +import { createAnomaliesResolvers } from './graphql/anomalies'; import { createAuthenticationsResolvers } from './graphql/authentications'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; @@ -32,6 +33,7 @@ import { createTlsResolvers } from './graphql/tls'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ + createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index c79b2651c11cb..2f1530a777042 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,13 +15,13 @@ import { timelineSavedObjectType, } from './saved_objects'; -import { signalsAlertType } from './lib/detection_engine/alerts/signals_alert_type'; +import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createSignalsRoute } from './lib/detection_engine/routes/create_signals_route'; -import { readSignalsRoute } from './lib/detection_engine/routes/read_signals_route'; -import { findSignalsRoute } from './lib/detection_engine/routes/find_signals_route'; -import { deleteSignalsRoute } from './lib/detection_engine/routes/delete_signals_route'; -import { updateSignalsRoute } from './lib/detection_engine/routes/update_signals_route'; +import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; const APP_ID = 'siem'; @@ -32,7 +32,8 @@ export const initServerWithKibana = ( mode: EnvironmentMode ) => { if (kbnServer.plugins.alerting != null) { - const type = signalsAlertType({ logger }); + const version = kbnServer.config().get('pkg.version'); + const type = rulesAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } @@ -48,13 +49,13 @@ export const initServerWithKibana = ( kbnServer.config().has('xpack.alerting.enabled') === true ) { logger.info( - 'Detected feature flags for actions and alerting and enabling signals API endpoints' + 'Detected feature flags for actions and alerting and enabling detection engine API endpoints' ); - createSignalsRoute(kbnServer); - readSignalsRoute(kbnServer); - updateSignalsRoute(kbnServer); - deleteSignalsRoute(kbnServer); - findSignalsRoute(kbnServer); + createRulesRoute(kbnServer); + readRulesRoute(kbnServer); + updateRulesRoute(kbnServer); + deleteRulesRoute(kbnServer); + findRulesRoute(kbnServer); } const xpackMainPlugin = kbnServer.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..f4b7aff4854e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { AnomaliesOverTimeData } from '../../graphql/types'; +import { inspectStringifyObject } from '../../utils/build_query'; +import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TermAggregation } from '../types'; + +import { AnomalyHit, AnomaliesAdapter, AnomaliesActionGroupData } from './types'; +import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; +import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; + +export class ElasticsearchAnomaliesAdapter implements AnomaliesAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getAnomaliesOverTime( + request: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + const dsl = buildAnomaliesOverTimeQuery(options); + + const response = await this.framework.callWithRequest( + request, + 'search', + dsl + ); + + const totalCount = getOr(0, 'hits.total.value', response); + const anomaliesOverTimeBucket = getOr([], 'aggregations.anomalyActionGroup.buckets', response); + + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + return { + inspect, + anomaliesOverTime: getAnomaliesOverTimeByJobId(anomaliesOverTimeBucket), + totalCount, + }; + } +} + +const getAnomaliesOverTimeByJobId = ( + data: AnomaliesActionGroupData[] +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach(({ key: group, anomalies }) => { + const anomaliesData = getOr([], 'buckets', anomalies).map( + ({ key, doc_count }: { key: number; doc_count: number }) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...anomaliesData]; + }); + + return result; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts new file mode 100644 index 0000000000000..7beeea4ad9e4e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +export * from './elasticsearch_adapter'; +import { AnomaliesAdapter } from './types'; +import { AnomaliesOverTimeData } from '../../../public/graphql/types'; + +export class Anomalies { + constructor(private readonly adapter: AnomaliesAdapter) {} + + public async getAnomaliesOverTime( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + return this.adapter.getAnomaliesOverTime(req, options); + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts new file mode 100644 index 0000000000000..34a6a6a8f601f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createQueryFilterClauses, calculateTimeseriesInterval } from '../../utils/build_query'; +import { RequestBasicOptions } from '../framework'; + +export const buildAnomaliesOverTimeQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: RequestBasicOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + timestamp: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeseriesInterval(from, to); + const histogramTimestampField = 'timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: `${interval}s`, + }, + }; + const autoDateHistogram = { + auto_date_histogram: { + field: histogramTimestampField, + buckets: 36, + }, + }; + return { + anomalyActionGroup: { + terms: { + field: 'job_id', + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + anomalies: interval ? dateHistogram : autoDateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts new file mode 100644 index 0000000000000..1e13ad88f8af3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnomaliesOverTimeData } from '../../graphql/types'; +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { SearchHit } from '../types'; + +export interface AnomaliesAdapter { + getAnomaliesOverTime( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts index e2ff0013e063c..a6b788cb70657 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts @@ -27,8 +27,7 @@ export const buildAuthenticationsOverTimeQuery = ({ ]; const getHistogramAggregation = () => { - const minIntervalSeconds = 10; - const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds); + const interval = calculateTimeseriesInterval(from, to); const histogramTimestampField = '@timestamp'; const dateHistogram = { date_histogram: { diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index c09db5bce5cc2..6e0c5e98206e4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -6,6 +6,8 @@ import { EnvironmentMode } from 'src/core/server'; import { ServerFacade } from '../../types'; +import { Anomalies } from '../anomalies'; +import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; import { KibanaConfigurationAdapter } from '../configuration/kibana_configuration_adapter'; @@ -43,6 +45,7 @@ export function compose(server: ServerFacade, mode: EnvironmentMode): AppBackend const pinnedEvent = new PinnedEvent({ savedObjects: framework.getSavedObjectsService() }); const domainLibs: AppDomainLibs = { + anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)), authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 5b2318e15a469..4b1dbf62d0dd4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -1,6 +1,45 @@ -Temporary README.md for developers working on the backend detection engine +Temporary README.md for users and developers working on the backend detection engine for how to get started. +# Setup for Users + +If you're just a user and want to enable the REST interfaces and UI screens do the following. +NOTE: this is very temporary and once alerting and actions is enabled by default you will no +longer have to do these steps + +Set the environment variable ALERTING_FEATURE_ENABLED to be true in your .profile or your windows +global environment variable. + +```sh +export ALERTING_FEATURE_ENABLED=true +``` + +In your `kibana.yml` file enable alerting and actions like so: + +```sh +# Feature flag to turn on alerting +xpack.alerting.enabled: true + +# Feature flag to turn on actions which goes with alerting +xpack.actions.enabled: true +``` + +Start Kibana and you will see these messages indicating detection engine is activated like so: + +```sh +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints +``` + +If you see crashes like this: + +```ts + FATAL Error: Unmet requirement "alerting" for plugin "siem" +``` + +It is because Kibana is not picking up your changes from `kibana.yml` and not seeing that alerting and actions is enabled. + +# For Developers + See these two other pages for references: https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/actions @@ -19,7 +58,7 @@ brew install jq Open up your .zshrc/.bashrc and add these lines with the variables filled in: -``` +```sh export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} export ELASTICSEARCH_URL=https://${ip}:9200 @@ -37,50 +76,44 @@ source your .zhsrc/.bashrc or open a new terminal to ensure you get the new valu Optional env var when set to true will utilize `reindex` api for reindexing instead of the scroll and bulk index combination. -``` +```sh export USE_REINDEX_API=true ``` Add these lines to your `kibana.dev.yml` to turn on the feature toggles of alerting and actions: -``` +```sh # Feature flag to turn on alerting xpack.alerting.enabled: true # Feature flag to turn on actions which goes with alerting xpack.actions.enabled: true - -# White list everything for ease of development (do not do in production) -xpack.actions.whitelistedHosts: ['*'] ``` -Open `x-pack/legacy/plugins/siem/index.ts` and find these lines and add the require statement -while commenting out the other require statement: +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. You should see alerting and actions starting up like so afterwards -``` -// Uncomment these lines to turn on alerting and action for detection engine and comment the other -// require statement out. These are hidden behind feature flags at the moment so if you turn -// these on without the feature flags turned on then Kibana will crash since we are a legacy plugin -// and legacy plugins cannot have optional requirements. -// require: ['kibana', 'elasticsearch', 'alerting', 'actions'], +```sh +server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready +server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -Restart Kibana and you should see alerting and actions starting up +You should also see the SIEM detect the feature flags and start the API endpoints for detection engine -``` -server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready -server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready +```sh +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` -You should also see the SIEM detect the feature flags and start the API endpoints for signals +Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same +value as you did with the environment variable of SIGNALS_INDEX, which should be `.siem-signals-${your user id}` ``` -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +.siem-signals-${your user id} ``` Open a terminal and go into the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: -``` +```sh ./hard_reset.sh ./post_signal.sh ``` @@ -91,39 +124,77 @@ which will: - Delete any existing alerts you have - Delete any existing alert tasks you have - Delete any existing signal mapping you might have had. -- Add the latest signal index and its mappings -- Posts a sample signal which checks for root or admin every 5 minutes +- Add the latest signal index and its mappings using your settings from `SIGNALS_INDEX` environment variable. +- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run ```sh -./get_alert_instances.sh +./find_rules.sh ``` -You should see the new alert instance created like so: +You should see the new rules created like so: -```ts +```sh { - "id": "908a6af1-ac63-4d52-a856-fc635a00db0f", - "alertTypeId": "siem.signals", - "interval": "5m", - "actions": [ ], - "alertTypeParams": {}, - "enabled": true, - "throttle": null, - "createdBy": "elastic", - "updatedBy": "elastic", - "apiKeyOwner": "elastic", - "scheduledTaskId": "4f401ca0-e402-11e9-94ed-051d758a6c79" + "page": 1, + "perPage": 20, + "total": 1, + "data": [ + { + "created_by": "elastic", + "description": "Detecting root and admin users", + "enabled": true, + "false_positives": [], + "from": "now-6m", + "id": "a556065c-0656-4ba1-ad64-a77ca9d2013b", + "immutable": false, + "index": [ + "auditbeat-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "interval": "5m", + "rule_id": "rule-1", + "language": "kuery", + "output_index": ".siem-signals-frank-hassanabad", + "max_signals": 100, + "risk_score": 1, + "name": "Detect Root/Admin Users", + "query": "user.name: root or user.name: admin", + "references": [ + "http://www.example.com", + "https://ww.example.com" + ], + "severity": "high", + "updated_by": "elastic", + "tags": [], + "to": "now", + "type": "query" + } + ] } ``` -Every 5 minutes you should see this message in your terminal now: +Every 5 minutes if you get positive hits you will see messages on info like so: +```sh +server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` -server log [22:17:33.945] [info][alerting] SIEM Alert Fired + +Rules are space aware and default to the "default" space for these scripts if you do not export +the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would +set your SPACE_URL to be: + +```sh +export SPACE_URL=/s/test-space ``` +So that the scripts prepend a `/s/test-space` in front of all the APIs to correctly create, modify, delete, and update +them from within that space. + See the scripts folder and the tools for more command line fun. Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 76df961535fcd..8080bd5ddd913 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, SignalAlertParams } from '../types'; +import { + SignalSourceHit, + SignalSearchResponse, + RuleTypeParams, + OutputRuleAlertRest, +} from '../types'; -export const sampleSignalAlertParams = (maxSignals: number | undefined): SignalAlertParams => ({ +export const sampleRuleAlertParams = ( + maxSignals?: number | undefined, + riskScore?: number | undefined +): RuleTypeParams => ({ ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], immutable: false, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', type: 'query', from: 'now-6m', tags: ['some fake tag'], @@ -21,39 +27,51 @@ export const sampleSignalAlertParams = (maxSignals: number | undefined): SignalA severity: 'high', query: 'user.name: root or user.name: admin', language: 'kuery', + outputIndex: '.siem-signals', references: ['http://google.com'], + riskScore: riskScore ? riskScore : 50, maxSignals: maxSignals ? maxSignals : 10000, - enabled: true, filter: undefined, filters: undefined, savedId: undefined, - size: 1000, + meta: undefined, }); -export const sampleDocNoSortId: SignalSourceHit = { +export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, -}; +}); + +export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _id: someUuid, + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, +}); -export const sampleDocWithSortId: SignalSourceHit = { +export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, sort: ['1234567891111'], -}; +}); export const sampleEmptyDocSearchResults: SignalSearchResponse = { took: 10, @@ -71,7 +89,63 @@ export const sampleEmptyDocSearchResults: SignalSearchResponse = { }, }; -export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { +export const sampleBulkCreateDuplicateResult = { + took: 60, + errors: true, + items: [ + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + _version: 1, + result: 'created', + _shards: { + total: 2, + successful: 1, + failed: 0, + }, + _seq_no: 1, + _primary_term: 1, + status: 201, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + ], +}; + +export const sampleDocSearchResultsNoSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -85,13 +159,37 @@ export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); + +export const sampleDocSearchResultsNoSortIdNoVersion = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 100, + max_score: 100, + hits: [ + { + ...sampleDocNoSortIdNoVersion(someUuid), + }, + ], + }, +}); -export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { +export const sampleDocSearchResultsNoSortIdNoHits = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -105,13 +203,17 @@ export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); -export const repeatedSearchResultsWithSortId = (repeat: number) => ({ +export const repeatedSearchResultsWithSortId = ( + total: number, + pageSize: number, + guids: string[] +) => ({ took: 10, timed_out: false, _shards: { @@ -121,15 +223,17 @@ export const repeatedSearchResultsWithSortId = (repeat: number) => ({ skipped: 0, }, hits: { - total: repeat, + total, max_score: 100, - hits: Array.from({ length: repeat }).map(x => ({ - ...sampleDocWithSortId, + hits: Array.from({ length: pageSize }).map((x, index) => ({ + ...sampleDocWithSortId(guids[index]), })), }, }); -export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { +export const sampleDocSearchResultsWithSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -143,10 +247,38 @@ export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocWithSortId, + ...sampleDocWithSortId(someUuid), }, ], }, -}; +}); -export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; + +export const sampleRule = (): Partial => { + return { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts deleted file mode 100644 index 0e8d95e4f7ac1..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts +++ /dev/null @@ -1,146 +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. - */ - -// TODO: Re-index is just a temporary solution in order to speed up development -// of any front end pieces. This should be replaced with a combination of the file -// build_events_query.ts and any scrolling/scaling solutions from that particular -// file. - -interface BuildEventsReIndexParams { - description: string; - index: string[]; - from: string; - to: string; - signalsIndex: string; - maxDocs: number; - filter: unknown; - severity: string; - name: string; - timeDetected: string; - ruleRevision: number; - id: string; - ruleId: string | undefined | null; - type: string; - references: string[]; -} - -export const buildEventsReIndex = ({ - description, - index, - from, - to, - signalsIndex, - maxDocs, - filter, - severity, - name, - timeDetected, - ruleRevision, - id, - ruleId, - type, - references, -}: BuildEventsReIndexParams) => { - const indexPatterns = index.map(element => `"${element}"`).join(','); - const refs = references.map(element => `"${element}"`).join(','); - const filterWithTime = [ - filter, - { - bool: { - filter: [ - { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: from, - }, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - range: { - '@timestamp': { - lte: to, - }, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - ]; - return { - body: { - source: { - index, - sort: [ - { - '@timestamp': 'desc', - }, - { - _doc: 'desc', - }, - ], - query: { - bool: { - filter: [ - ...filterWithTime, - { - match_all: {}, - }, - ], - }, - }, - }, - dest: { - index: signalsIndex, - }, - script: { - source: ` - String[] indexPatterns = new String[] {${indexPatterns}}; - String[] references = new String[] {${refs}}; - - def parent = [ - "id": ctx._id, - "type": "event", - "index": ctx._index, - "depth": 1 - ]; - - def signal = [ - "id": "${id}", - "rule_revision": "${ruleRevision}", - "rule_id": "${ruleId}", - "rule_type": "${type}", - "parent": parent, - "name": "${name}", - "severity": "${severity}", - "description": "${description}", - "original_time": ctx._source['@timestamp'], - "index_patterns": indexPatterns, - "references": references - ]; - - ctx._source.signal = signal; - ctx._source['@timestamp'] = "${timeDetected}"; - `, - lang: 'painless', - }, - max_docs: maxDocs, - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts new file mode 100644 index 0000000000000..7c66714484383 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SIGNALS_ID } from '../../../../common/constants'; +import { RuleParams } from './types'; + +export const createRules = async ({ + alertsClient, + actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... + description, + enabled, + falsePositives, + filter, + from, + query, + language, + savedId, + meta, + filters, + ruleId, + immutable, + index, + interval, + maxSignals, + riskScore, + outputIndex, + name, + severity, + tags, + to, + type, + references, +}: RuleParams) => { + return alertsClient.create({ + data: { + name, + tags: [], + alertTypeId: SIGNALS_ID, + params: { + description, + ruleId, + index, + falsePositives, + from, + filter, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + maxSignals, + riskScore, + severity, + tags, + to, + type, + references, + }, + interval, + enabled, + actions: [], // TODO: Create and add actions here once we have email, etc... + throttle: null, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts deleted file mode 100644 index d5163cb1fcbb6..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ /dev/null @@ -1,65 +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 { SIGNALS_ID } from '../../../../common/constants'; -import { SignalParams } from './types'; - -export const createSignals = async ({ - alertsClient, - actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... - description, - enabled, - falsePositives, - filter, - from, - query, - language, - savedId, - filters, - ruleId, - immutable, - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, -}: SignalParams) => { - return alertsClient.create({ - data: { - name, - tags: [], - alertTypeId: SIGNALS_ID, - alertTypeParams: { - description, - ruleId, - index, - falsePositives, - from, - filter, - immutable, - query, - language, - savedId, - filters, - maxSignals, - severity, - tags, - to, - type, - references, - }, - interval, - enabled, - actions: [], // TODO: Create and add actions here once we have email, etc... - throttle: null, - }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts new file mode 100644 index 0000000000000..c3ca1d79424cf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readRules } from './read_rules'; +import { DeleteRuleParams } from './types'; + +export const deleteRules = async ({ + alertsClient, + actionsClient, // TODO: Use this when we have actions such as email, etc... + id, + ruleId, +}: DeleteRuleParams) => { + const rule = await readRules({ alertsClient, id, ruleId }); + if (rule == null) { + return null; + } + + if (ruleId != null) { + await alertsClient.delete({ id: rule.id }); + return rule; + } else if (id != null) { + try { + await alertsClient.delete({ id }); + return rule; + } catch (err) { + if (err.output.statusCode === 404) { + return null; + } else { + throw err; + } + } + } else { + return null; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts deleted file mode 100644 index d89895772f1ef..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { readSignals } from './read_signals'; -import { DeleteSignalParams } from './types'; - -export const deleteSignals = async ({ - alertsClient, - actionsClient, // TODO: Use this when we have actions such as email, etc... - id, - ruleId, -}: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id, ruleId }); - if (signal == null) { - return null; - } - - if (ruleId != null) { - await alertsClient.delete({ id: signal.id }); - return signal; - } else if (id != null) { - try { - await alertsClient.delete({ id }); - return signal; - } catch (err) { - if (err.output.statusCode === 404) { - return null; - } else { - throw err; - } - } - } else { - return null; - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts new file mode 100644 index 0000000000000..23f031b22a9dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getFilter } from './find_rules'; +import { SIGNALS_ID } from '../../../../common/constants'; + +describe('find_rules', () => { + test('it returns a full filter with an AND if sent down', () => { + expect(getFilter('alert.attributes.enabled: true')).toEqual( + `alert.attributes.alertTypeId: ${SIGNALS_ID} AND alert.attributes.enabled: true` + ); + }); + + test('it returns existing filter with no AND when not set', () => { + expect(getFilter(null)).toEqual(`alert.attributes.alertTypeId: ${SIGNALS_ID}`); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts new file mode 100644 index 0000000000000..c1058bd353e8c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SIGNALS_ID } from '../../../../common/constants'; +import { FindRuleParams } from './types'; + +export const getFilter = (filter: string | null | undefined) => { + if (filter == null) { + return `alert.attributes.alertTypeId: ${SIGNALS_ID}`; + } else { + return `alert.attributes.alertTypeId: ${SIGNALS_ID} AND ${filter}`; + } +}; + +export const findRules = async ({ + alertsClient, + perPage, + page, + fields, + filter, + sortField, + sortOrder, +}: FindRuleParams) => { + return alertsClient.find({ + options: { + fields, + page, + perPage, + filter: getFilter(filter), + sortOrder, + sortField, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts deleted file mode 100644 index 23f4e38a95eea..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SIGNALS_ID } from '../../../../common/constants'; -import { FindSignalParams } from './types'; - -export const findSignals = async ({ alertsClient, perPage, page, fields }: FindSignalParams) => - alertsClient.find({ - options: { - fields, - page, - perPage, - filter: `alert.attributes.alertTypeId: ${SIGNALS_ID}`, - }, - }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts index 9f72da44e963b..c55c99fb291c4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { AlertServices } from '../../../../../alerting/server/types'; +import { PartialFilter } from './types'; describe('get_filter', () => { let savedObjectsClient = savedObjectsClientMock.create(); @@ -145,6 +146,103 @@ describe('get_filter', () => { }); }); + test('it should work with a simple filter as a kuery without meta information', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery without meta information with an exists', () => { + const query: PartialFilter = { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + } as PartialFilter; + + const exists: PartialFilter = { + exists: { + field: 'host.hostname', + }, + } as PartialFilter; + + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [query, exists], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + { + exists: { + field: 'host.hostname', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + test('it should work with a simple filter that is disabled as a kuery', () => { const esQuery = getQueryFilter( 'host.name: windows', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index bc5d11d70586d..5d3b47ecebfd5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalAlertParams, PartialFilter } from './types'; +import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -41,7 +41,7 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: SignalAlertParams['type']; + type: RuleAlertParams['type']; filter: Record | undefined | null; filters: PartialFilter[] | undefined | null; language: string | undefined | null; @@ -71,13 +71,26 @@ export const getFilter = async ({ } case 'saved_query': { if (savedId != null && index != null) { - const savedObject = await services.savedObjectsClient.get('query', savedId); - return getQueryFilter( - savedObject.attributes.query.query, - savedObject.attributes.query.language, - savedObject.attributes.filters, - index - ); + try { + // try to get the saved object first + const savedObject = await services.savedObjectsClient.get('query', savedId); + return getQueryFilter( + savedObject.attributes.query.query, + savedObject.attributes.query.language, + savedObject.attributes.filters, + index + ); + } catch (err) { + // saved object does not exist, so try and fall back if the user pushed + // any additional language, query, filters, etc... + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index); + } else { + // user did not give any additional fall back mechanism for generating a rule + // rethrow error for activity monitoring + throw err; + } + } } else { throw new TypeError('savedId parameter should be defined'); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts new file mode 100644 index 0000000000000..07eb7c885b443 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; + +describe('get_input_output_index', () => { + let savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + let servicesMock: AlertServices = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + servicesMock = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + }); + + describe('getInputOutputIndex', () => { + test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + null + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + null, + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns a saved object outputIndex if both passed in are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + 'output-index-1' + ); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); + + describe('getOutputIndex', () => { + test('test output index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex('output-index-1', mockConfiguration); + expect(outputIndex).toEqual('output-index-1'); + }); + + test('configured output index is returned when output index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual('.siem-test-signals'); + }); + + test('output index from constants is returned when output index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + }); + + describe('getInputIndex', () => { + test('test input index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); + expect(inputIndex).toEqual(['input-index-1']); + }); + + test('configured input index is returned when input index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); + }); + + test('input index from constants is returned when input index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts new file mode 100644 index 0000000000000..567ab27976d8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; + +interface IndexObjectAttributes extends SavedObjectAttributes { + [DEFAULT_INDEX_KEY]: string[]; + [DEFAULT_SIGNALS_INDEX_KEY]: string; +} + +export const getInputIndex = ( + inputIndex: string[] | undefined | null, + configuration: SavedObject +): string[] => { + if (inputIndex != null) { + return inputIndex; + } else { + if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { + return configuration.attributes[DEFAULT_INDEX_KEY]; + } else { + return defaultIndexPattern; + } + } +}; + +export const getOutputIndex = ( + outputIndex: string | undefined | null, + configuration: SavedObject +): string => { + if (outputIndex != null) { + return outputIndex; + } else { + if ( + configuration.attributes != null && + configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null + ) { + return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; + } else { + return DEFAULT_SIGNALS_INDEX; + } + } +}; + +export const getInputOutputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined, + outputIndex: string | null | undefined +): Promise<{ + inputIndex: string[]; + outputIndex: string; +}> => { + if (inputIndex != null && outputIndex != null) { + return { inputIndex, outputIndex }; + } else { + const configuration = await services.savedObjectsClient.get('config', version); + return { + inputIndex: getInputIndex(inputIndex, configuration), + outputIndex: getOutputIndex(outputIndex, configuration), + }; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts new file mode 100644 index 0000000000000..b3d7ab1322775 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; +import { AlertsClient } from '../../../../../alerting'; +import { + getResult, + getFindResultWithSingleHit, + getFindResultWithMultiHits, +} from '../routes/__mocks__/request_responses'; +import { SIGNALS_ID } from '../../../../common/constants'; + +describe('read_rules', () => { + describe('readRules', () => { + test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(rule).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is set but ruleId is null', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: null, + }); + expect(rule).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: undefined, + ruleId: 'rule-1', + }); + expect(rule).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is null but ruleId is set', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: null, + ruleId: 'rule-1', + }); + expect(rule).toEqual(getResult()); + }); + + test('should return null if id and ruleId are null', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: null, + ruleId: null, + }); + expect(rule).toEqual(null); + }); + + test('should return null if id and ruleId are undefined', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRules({ + alertsClient: unsafeCast, + id: undefined, + ruleId: undefined, + }); + expect(rule).toEqual(null); + }); + }); + + describe('readRuleByRuleId', () => { + test('should return a single value if the rule id matches', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRuleByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-1', + }); + expect(rule).toEqual(getResult()); + }); + + test('should not return a single value if the rule id does not match', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRuleByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-that-should-not-match-anything', + }); + expect(rule).toEqual(null); + }); + + test('should return a single value of rule-1 with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRuleByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-1', + }); + expect(rule).toEqual(result1); + }); + + test('should return a single value of rule-2 with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRuleByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-2', + }); + expect(rule).toEqual(result2); + }); + + test('should return null for a made up value with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const rule = await readRuleByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-that-should-not-match-anything', + }); + expect(rule).toEqual(null); + }); + }); + + describe('findRuleInArrayByRuleId', () => { + test('returns null if the objects are not of a signal rule type', () => { + const rule = findRuleInArrayByRuleId( + [ + { alertTypeId: 'made up 1', params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, + ], + '123' + ); + expect(rule).toEqual(null); + }); + + test('returns correct type if the objects are of a signal rule type', () => { + const rule = findRuleInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, + ], + '123' + ); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); + }); + + test('returns second correct type if the objects are of a signal rule type', () => { + const rule = findRuleInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, + ], + '456' + ); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); + }); + + test('returns null with correct types but data does not exist', () => { + const rule = findRuleInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, + ], + '892' + ); + expect(rule).toEqual(null); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts new file mode 100644 index 0000000000000..5c33526329016 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { findRules } from './find_rules'; +import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; + +export const findRuleInArrayByRuleId = ( + objects: object[], + ruleId: string +): RuleAlertType | null => { + if (isAlertTypeArray(objects)) { + const rules: RuleAlertType[] = objects; + const rule: RuleAlertType[] = rules.filter(datum => { + return datum.params.ruleId === ruleId; + }); + if (rule.length !== 0) { + return rule[0]; + } else { + return null; + } + } else { + return null; + } +}; + +// This an extremely slow and inefficient way of getting a rule by its id. +// I have to manually query every single record since the rule Params are +// not indexed and I cannot push in my own _id when I create an alert at the moment. +// TODO: Once we can directly push in the _id, then we should no longer need this way. +// TODO: This is meant to be _very_ temporary. +export const readRuleByRuleId = async ({ + alertsClient, + ruleId, +}: ReadRuleByRuleId): Promise => { + const firstRules = await findRules({ alertsClient, page: 1 }); + const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); + if (firstRule != null) { + return firstRule; + } else { + const totalPages = Math.ceil(firstRules.total / firstRules.perPage); + return Array(totalPages) + .fill({}) + .map((_, page) => { + // page index never starts at zero. It always has to be 1 or greater + return findRules({ alertsClient, page: page + 1 }); + }) + .reduce>(async (accum, findRule) => { + const rules = await findRule; + const rule = findRuleInArrayByRuleId(rules.data, ruleId); + if (rule != null) { + return rule; + } else { + return accum; + } + }, Promise.resolve(null)); + } +}; + +export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { + if (id != null) { + try { + const output = await alertsClient.get({ id }); + return output; + } catch (err) { + if (err.output.statusCode === 404) { + return null; + } else { + // throw non-404 as they would be 500 or other internal errors + throw err; + } + } + } else if (ruleId != null) { + return readRuleByRuleId({ alertsClient, ruleId }); + } else { + // should never get here, and yet here we are. + return null; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts deleted file mode 100644 index dde3f19b1c66d..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ /dev/null @@ -1,238 +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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; -import { AlertsClient } from '../../../../../alerting'; -import { - getResult, - getFindResultWithSingleHit, - getFindResultWithMultiHits, -} from '../routes/__mocks__/request_responses'; -import { SIGNALS_ID } from '../../../../common/constants'; - -describe('read_signals', () => { - describe('readSignals', () => { - test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ruleId: undefined, - }); - expect(signal).toEqual(getResult()); - }); - - test('should return the output from alertsClient if id is set but ruleId is null', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ruleId: null, - }); - expect(signal).toEqual(getResult()); - }); - - test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: undefined, - ruleId: 'rule-1', - }); - expect(signal).toEqual(getResult()); - }); - - test('should return the output from alertsClient if id is null but ruleId is set', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: null, - ruleId: 'rule-1', - }); - expect(signal).toEqual(getResult()); - }); - - test('should return null if id and ruleId are null', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: null, - ruleId: null, - }); - expect(signal).toEqual(null); - }); - - test('should return null if id and ruleId are undefined', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ - alertsClient: unsafeCast, - id: undefined, - ruleId: undefined, - }); - expect(signal).toEqual(null); - }); - }); - - describe('readSignalByRuleId', () => { - test('should return a single value if the rule id matches', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(signal).toEqual(getResult()); - }); - - test('should not return a single value if the rule id does not match', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(signal).toEqual(null); - }); - - test('should return a single value of rule-1 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(signal).toEqual(result1); - }); - - test('should return a single value of rule-2 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-2', - }); - expect(signal).toEqual(result2); - }); - - test('should return null for a made up value with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(signal).toEqual(null); - }); - }); - - describe('findSignalInArrayByRuleId', () => { - test('returns null if the objects are not of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( - [ - { alertTypeId: 'made up 1', alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, - ], - '123' - ); - expect(signal).toEqual(null); - }); - - test('returns correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, - ], - '123' - ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '123' } }); - }); - - test('returns second correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, - ], - '456' - ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '456' } }); - }); - - test('returns null with correct types but data does not exist', () => { - const signal = findSignalInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, - ], - '892' - ); - expect(signal).toEqual(null); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts deleted file mode 100644 index f73074b560cb2..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ /dev/null @@ -1,81 +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 { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; - -export const findSignalInArrayByRuleId = ( - objects: object[], - ruleId: string -): SignalAlertType | null => { - if (isAlertTypeArray(objects)) { - const signals: SignalAlertType[] = objects; - const signal: SignalAlertType[] = signals.filter(datum => { - return datum.alertTypeParams.ruleId === ruleId; - }); - if (signal.length !== 0) { - return signal[0]; - } else { - return null; - } - } else { - return null; - } -}; - -// This an extremely slow and inefficient way of getting a signal by its id. -// I have to manually query every single record since the Signal Params are -// not indexed and I cannot push in my own _id when I create an alert at the moment. -// TODO: Once we can directly push in the _id, then we should no longer need this way. -// TODO: This is meant to be _very_ temporary. -export const readSignalByRuleId = async ({ - alertsClient, - ruleId, -}: ReadSignalByRuleId): Promise => { - const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); - if (firstSignal != null) { - return firstSignal; - } else { - const totalPages = Math.ceil(firstSignals.total / firstSignals.perPage); - return Array(totalPages) - .fill({}) - .map((_, page) => { - // page index never starts at zero. It always has to be 1 or greater - return findSignals({ alertsClient, page: page + 1 }); - }) - .reduce>(async (accum, findSignal) => { - const signals = await findSignal; - const signal = findSignalInArrayByRuleId(signals.data, ruleId); - if (signal != null) { - return signal; - } else { - return accum; - } - }, Promise.resolve(null)); - } -}; - -export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { - if (id != null) { - try { - const output = await alertsClient.get({ id }); - return output; - } catch (err) { - if (err.output.statusCode === 404) { - return null; - } else { - // throw non-404 as they would be 500 or other internal errors - throw err; - } - } - } else if (ruleId != null) { - return readSignalByRuleId({ alertsClient, ruleId }); - } else { - // should never get here, and yet here we are. - return null; - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts new file mode 100644 index 0000000000000..91d7d18a4945c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { Logger } from 'src/core/server'; +import { + SIGNALS_ID, + DEFAULT_MAX_SIGNALS, + DEFAULT_SEARCH_AFTER_PAGE_SIZE, +} from '../../../../common/constants'; + +import { buildEventsSearchQuery } from './build_events_query'; +import { searchAfterAndBulkCreate } from './utils'; +import { RuleAlertTypeDefinition } from './types'; +import { getFilter } from './get_filter'; +import { getInputOutputIndex } from './get_input_output_index'; + +export const rulesAlertType = ({ + logger, + version, +}: { + logger: Logger; + version: string; +}): RuleAlertTypeDefinition => { + return { + id: SIGNALS_ID, + name: 'SIEM Signals', + actionGroups: ['default'], + validate: { + params: schema.object({ + description: schema.string(), + falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), + from: schema.string(), + filter: schema.nullable(schema.object({}, { allowUnknowns: true })), + ruleId: schema.string(), + immutable: schema.boolean({ defaultValue: false }), + index: schema.nullable(schema.arrayOf(schema.string())), + language: schema.nullable(schema.string()), + outputIndex: schema.nullable(schema.string()), + savedId: schema.nullable(schema.string()), + meta: schema.nullable(schema.object({}, { allowUnknowns: true })), + query: schema.nullable(schema.string()), + filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), + riskScore: schema.number(), + severity: schema.string(), + tags: schema.arrayOf(schema.string(), { defaultValue: [] }), + to: schema.string(), + type: schema.string(), + references: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), + }, + async executor({ alertId, services, params }) { + const { + filter, + from, + ruleId, + index, + filters, + language, + outputIndex, + savedId, + query, + to, + type, + } = params; + + // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 + const savedObject = await services.savedObjectsClient.get('alert', alertId); + const name: string = savedObject.attributes.name; + + const createdBy: string = savedObject.attributes.createdBy; + const updatedBy: string = savedObject.attributes.updatedBy; + const interval: string = savedObject.attributes.interval; + const enabled: boolean = savedObject.attributes.enabled; + + // set searchAfter page size to be the lesser of default page size or maxSignals. + const searchAfterSize = + DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals + ? DEFAULT_SEARCH_AFTER_PAGE_SIZE + : params.maxSignals; + + const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( + services, + version, + index, + outputIndex + ); + const esFilter = await getFilter({ + type, + filter, + filters, + language, + query, + savedId, + services, + index: inputIndex, + }); + + const noReIndex = buildEventsSearchQuery({ + index: inputIndex, + from, + to, + filter: esFilter, + size: searchAfterSize, + searchAfterSortId: undefined, + }); + + try { + logger.debug(`Starting signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); + logger.debug( + `[+] Initial search call of signal rule "id: ${alertId}", "ruleId: ${ruleId}"` + ); + const noReIndexResult = await services.callCluster('search', noReIndex); + if (noReIndexResult.hits.total.value !== 0) { + logger.info( + `Found ${ + noReIndexResult.hits.total.value + } signals from the indexes of "${inputIndex.join( + ', ' + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` + ); + } + + const bulkIndexResult = await searchAfterAndBulkCreate({ + someResult: noReIndexResult, + ruleParams: params, + services, + logger, + id: alertId, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + pageSize: searchAfterSize, + }); + + if (bulkIndexResult) { + logger.debug(`Finished signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); + } else { + logger.error(`Error processing signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); + } + } catch (err) { + // TODO: Error handling and writing of errors into a signal that has error + // handling/conditions + logger.error( + `Error from signal rule "id: ${alertId}", "ruleId: ${ruleId}", ${err.message}` + ); + } + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts deleted file mode 100644 index d19b8f1c4ad60..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; -import { SIGNALS_ID, DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -// TODO: Remove this for the build_events_query call eventually -import { buildEventsReIndex } from './build_events_reindex'; - -import { buildEventsSearchQuery } from './build_events_query'; -import { searchAfterAndBulkIndex } from './utils'; -import { SignalAlertTypeDefinition } from './types'; -import { getFilter } from './get_filter'; - -export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTypeDefinition => { - return { - id: SIGNALS_ID, - name: 'SIEM Signals', - actionGroups: ['default'], - validate: { - params: schema.object({ - description: schema.string(), - falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), - from: schema.string(), - filter: schema.nullable(schema.object({}, { allowUnknowns: true })), - ruleId: schema.string(), - immutable: schema.boolean({ defaultValue: false }), - index: schema.arrayOf(schema.string()), - language: schema.nullable(schema.string()), - savedId: schema.nullable(schema.string()), - query: schema.nullable(schema.string()), - filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: 10000 }), - severity: schema.string(), - tags: schema.arrayOf(schema.string(), { defaultValue: [] }), - to: schema.string(), - type: schema.string(), - references: schema.arrayOf(schema.string(), { defaultValue: [] }), - size: schema.maybe(schema.number()), - }), - }, - async executor({ alertId, services, params }) { - const { - description, - filter, - from, - ruleId, - index, - filters, - language, - savedId, - query, - maxSignals, - references, - severity, - to, - type, - size, - } = params; - - // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 - const savedObject = await services.savedObjectsClient.get('alert', alertId); - const name = savedObject.attributes.name; - const searchAfterSize = size ? size : 1000; - - const esFilter = await getFilter({ - type, - filter, - filters, - language, - query, - savedId, - services, - index, - }); - - const noReIndex = buildEventsSearchQuery({ - index, - from, - to, - filter: esFilter, - size: searchAfterSize, - searchAfterSortId: undefined, - }); - - try { - logger.debug(`Starting signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); - if (process.env.USE_REINDEX_API === 'true') { - const reIndex = buildEventsReIndex({ - index, - from, - to, - // TODO: Change this out once we have solved - // https://github.com/elastic/kibana/issues/47002 - signalsIndex: process.env.SIGNALS_INDEX || DEFAULT_SIGNALS_INDEX, - severity, - description, - name, - timeDetected: new Date().toISOString(), - filter: esFilter, - maxDocs: maxSignals, - ruleRevision: 1, - id: alertId, - ruleId, - type, - references, - }); - const result = await services.callCluster('reindex', reIndex); - if (result.total > 0) { - logger.info( - `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}" (reindex algorithm): ${result.total}` - ); - } - } else { - logger.debug( - `[+] Initial search call of signal rule "id: ${alertId}", "ruleId: ${ruleId}"` - ); - const noReIndexResult = await services.callCluster('search', noReIndex); - if (noReIndexResult.hits.total.value !== 0) { - logger.info( - `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}": ${noReIndexResult.hits.total.value}` - ); - } - - const bulkIndexResult = await searchAfterAndBulkIndex( - noReIndexResult, - params, - services, - logger, - alertId - ); - - if (bulkIndexResult) { - logger.debug(`Finished signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); - } else { - logger.error(`Error processing signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); - } - } - } catch (err) { - // TODO: Error handling and writing of errors into a signal that has error - // handling/conditions - logger.error( - `Error from signal rule "id: ${alertId}", "ruleId: ${ruleId}", ${err.message}` - ); - } - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index a6cb56ada8df1..462a9b7d65ee2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,7 +21,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; -export interface SignalAlertParams { +export interface RuleAlertParams { description: string; enabled: boolean; falsePositives: string[]; @@ -34,43 +34,49 @@ export interface SignalAlertParams { ruleId: string | undefined | null; language: string | undefined | null; maxSignals: number; + riskScore: number; + outputIndex: string; name: string; query: string | undefined | null; references: string[]; savedId: string | undefined | null; + meta: Record | undefined | null; severity: string; - size: number | undefined | null; tags: string[]; to: string; type: 'filter' | 'query' | 'saved_query'; } -export type SignalAlertParamsRest = Omit< - SignalAlertParams, - 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' +export type RuleAlertParamsRest = Omit< + RuleAlertParams, + 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' > & { - rule_id: SignalAlertParams['ruleId']; - false_positives: SignalAlertParams['falsePositives']; - saved_id: SignalAlertParams['savedId']; - max_signals: SignalAlertParams['maxSignals']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; }; -export type OutputSignalAlertRest = SignalAlertParamsRest & { +export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; }; -export type UpdateSignalAlertParamsRest = Partial & { +export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; - rule_id: SignalAlertParams['ruleId'] | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { per_page: number; page: number; sort_field: string; + sort_order: 'asc' | 'desc'; fields: string[]; + filter: string; } export interface Clients { @@ -78,63 +84,67 @@ export interface Clients { actionsClient: ActionsClient; } -export type SignalParams = SignalAlertParams & Clients; +export type RuleParams = RuleAlertParams & Clients; -export type UpdateSignalParams = Partial & { +export type UpdateRuleParams = Partial & { id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { +export type DeleteRuleParams = Clients & { id: string | undefined; ruleId: string | undefined | null; }; -export interface FindSignalsRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; search?: string; sort_field?: string; + filter?: string; fields?: string[]; + sort_order?: 'asc' | 'desc'; }; } -export interface FindSignalParams { +export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; page?: number; sortField?: string; + filter?: string; fields?: string[]; + sortOrder?: 'asc' | 'desc'; } -export interface ReadSignalParams { +export interface ReadRuleParams { alertsClient: AlertsClient; id?: string | undefined | null; ruleId?: string | undefined | null; } -export interface ReadSignalByRuleId { +export interface ReadRuleByRuleId { alertsClient: AlertsClient; ruleId: string; } -export type AlertTypeParams = Omit; +export type RuleTypeParams = Omit; -export type SignalAlertType = Alert & { +export type RuleAlertType = Alert & { id: string; - alertTypeParams: AlertTypeParams; + params: RuleTypeParams; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalAlertParamsRest; +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; } -export interface UpdateSignalsRequest extends RequestFacade { - payload: UpdateSignalAlertParamsRest; +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; } -export type SignalExecutorOptions = Omit & { - params: SignalAlertParams & { +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { scrollSize: number; scrollLock: string; }; @@ -158,7 +168,46 @@ export interface SignalSource { export interface BulkResponse { took: number; errors: boolean; - items: unknown[]; + items: [ + { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; + } + ]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; } export type SignalSearchResponse = SearchResponse; @@ -168,24 +217,24 @@ export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -// This returns true because by default a SignalAlertTypeDefinition is an AlertType +// This returns true because by default a RuleAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { +export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { return true; }; -export type SignalAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: SignalExecutorOptions) => Promise; +export type RuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; }; -export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { - return obj.every(signal => isAlertType(signal)); +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is SignalAlertType => { +export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isAlertTypeArray = (objArray: unknown[]): objArray is SignalAlertType[] => { +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { return objArray.length === 0 || isAlertType(objArray[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts new file mode 100644 index 0000000000000..1022fea93200f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { calculateInterval, calculateName } from './update_rules'; + +describe('update_rules', () => { + describe('#calculateInterval', () => { + test('given a undefined interval, it returns the ruleInterval ', () => { + const interval = calculateInterval(undefined, '10m'); + expect(interval).toEqual('10m'); + }); + + test('given a undefined ruleInterval, it returns a undefined interval ', () => { + const interval = calculateInterval('10m', undefined); + expect(interval).toEqual('10m'); + }); + + test('given both an undefined ruleInterval and a undefined interval, it returns 5m', () => { + const interval = calculateInterval(undefined, undefined); + expect(interval).toEqual('5m'); + }); + }); + + describe('#calculateName', () => { + test('should return the updated name when it and originalName is there', () => { + const name = calculateName({ updatedName: 'updated', originalName: 'original' }); + expect(name).toEqual('updated'); + }); + + test('should return the updated name when originalName is undefined', () => { + const name = calculateName({ updatedName: 'updated', originalName: undefined }); + expect(name).toEqual('updated'); + }); + + test('should return the original name when updatedName is undefined', () => { + const name = calculateName({ updatedName: undefined, originalName: 'original' }); + expect(name).toEqual('original'); + }); + + test('should return untitled when both updatedName and originalName is undefined', () => { + const name = calculateName({ updatedName: undefined, originalName: undefined }); + expect(name).toEqual('untitled'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts new file mode 100644 index 0000000000000..81360d7824230 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { defaults } from 'lodash/fp'; +import { AlertAction } from '../../../../../alerting/server/types'; +import { readRules } from './read_rules'; +import { UpdateRuleParams } from './types'; + +export const calculateInterval = ( + interval: string | undefined, + ruleInterval: string | undefined +): string => { + if (interval != null) { + return interval; + } else if (ruleInterval != null) { + return ruleInterval; + } else { + return '5m'; + } +}; + +export const calculateName = ({ + updatedName, + originalName, +}: { + updatedName: string | undefined; + originalName: string | undefined; +}): string => { + if (updatedName != null) { + return updatedName; + } else if (originalName != null) { + return originalName; + } else { + // You really should never get to this point. This is a fail safe way to send back + // the name of "untitled" just in case a rule name became null or undefined at + // some point since TypeScript allows it. + return 'untitled'; + } +}; + +export const updateRules = async ({ + alertsClient, + actionsClient, // TODO: Use this whenever we add feature support for different action types + description, + falsePositives, + enabled, + query, + language, + outputIndex, + savedId, + meta, + filters, + filter, + from, + immutable, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + references, +}: UpdateRuleParams) => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { + return null; + } + + // TODO: Remove this as cast as soon as rule.actions TypeScript bug is fixed + // where it is trying to return AlertAction[] or RawAlertAction[] + const actions = (rule.actions as AlertAction[] | undefined) || []; + + const params = rule.params || {}; + + const nextParams = defaults( + { + ...params, + }, + { + description, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + index, + maxSignals, + riskScore, + severity, + tags, + to, + type, + references, + } + ); + + if (rule.enabled && !enabled) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled) { + await alertsClient.enable({ id: rule.id }); + } + + return alertsClient.update({ + id: rule.id, + data: { + tags: [], + name: calculateName({ updatedName: name, originalName: rule.name }), + interval: calculateInterval(interval, rule.interval), + actions, + params: nextParams, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts deleted file mode 100644 index 39f7951a8eab9..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { calculateInterval, calculateName } from './update_signals'; - -describe('update_signals', () => { - describe('#calculateInterval', () => { - test('given a undefined interval, it returns the signalInterval ', () => { - const interval = calculateInterval(undefined, '10m'); - expect(interval).toEqual('10m'); - }); - - test('given a undefined signalInterval, it returns a undefined interval ', () => { - const interval = calculateInterval('10m', undefined); - expect(interval).toEqual('10m'); - }); - - test('given both an undefined signalInterval and a undefined interval, it returns 5m', () => { - const interval = calculateInterval(undefined, undefined); - expect(interval).toEqual('5m'); - }); - }); - - describe('#calculateName', () => { - test('should return the updated name when it and originalName is there', () => { - const name = calculateName({ updatedName: 'updated', originalName: 'original' }); - expect(name).toEqual('updated'); - }); - - test('should return the updated name when originalName is undefined', () => { - const name = calculateName({ updatedName: 'updated', originalName: undefined }); - expect(name).toEqual('updated'); - }); - - test('should return the original name when updatedName is undefined', () => { - const name = calculateName({ updatedName: undefined, originalName: 'original' }); - expect(name).toEqual('original'); - }); - - test('should return untitled when both updatedName and originalName is undefined', () => { - const name = calculateName({ updatedName: undefined, originalName: undefined }); - expect(name).toEqual('untitled'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts deleted file mode 100644 index 38db25ca326b1..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ /dev/null @@ -1,120 +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 { defaults } from 'lodash/fp'; -import { AlertAction } from '../../../../../alerting/server/types'; -import { readSignals } from './read_signals'; -import { UpdateSignalParams } from './types'; - -export const calculateInterval = ( - interval: string | undefined, - signalInterval: string | undefined -): string => { - if (interval != null) { - return interval; - } else if (signalInterval != null) { - return signalInterval; - } else { - return '5m'; - } -}; - -export const calculateName = ({ - updatedName, - originalName, -}: { - updatedName: string | undefined; - originalName: string | undefined; -}): string => { - if (updatedName != null) { - return updatedName; - } else if (originalName != null) { - return originalName; - } else { - // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a signal rule name became null or undefined at - // some point since TypeScript allows it. - return 'untitled'; - } -}; - -export const updateSignal = async ({ - alertsClient, - actionsClient, // TODO: Use this whenever we add feature support for different action types - description, - falsePositives, - enabled, - query, - language, - savedId, - filters, - filter, - from, - immutable, - id, - ruleId, - index, - interval, - maxSignals, - name, - severity, - tags, - to, - type, - references, -}: UpdateSignalParams) => { - const signal = await readSignals({ alertsClient, ruleId, id }); - if (signal == null) { - return null; - } - - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed - // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; - - const alertTypeParams = signal.alertTypeParams || {}; - - const nextAlertTypeParams = defaults( - { - ...alertTypeParams, - }, - { - description, - falsePositives, - filter, - from, - immutable, - query, - language, - savedId, - filters, - index, - maxSignals, - severity, - tags, - to, - type, - references, - } - ); - - if (signal.enabled && !enabled) { - await alertsClient.disable({ id: signal.id }); - } else if (!signal.enabled && enabled) { - await alertsClient.enable({ id: signal.id }); - } - - return alertsClient.update({ - id: signal.id, - data: { - tags: [], - name: calculateName({ updatedName: name, originalName: signal.name }), - interval: calculateInterval(interval, signal.interval), - actions, - alertTypeParams: nextAlertTypeParams, - }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index a5e6d03a3378b..fc50e54e06e4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -3,25 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import uuid from 'uuid'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { Logger } from '../../../../../../../../src/core/server'; import { buildBulkBody, - singleBulkIndex, + generateId, + singleBulkCreate, singleSearchAfter, - searchAfterAndBulkIndex, + searchAfterAndBulkCreate, + buildEventTypeSignal, + buildSignal, + buildRule, } from './utils'; import { sampleDocNoSortId, - sampleSignalAlertParams, + sampleRuleAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, + sampleDocSearchResultsNoSortIdNoVersion, sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, - sampleSignalId, + sampleBulkCreateDuplicateResult, + sampleRuleGuid, + sampleRule, + sampleIdGuid, } from './__mocks__/es_results'; +import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { OutputRuleAlertRest } from './types'; +import { Signal } from '../../types'; const mockLogger: Logger = { log: jest.fn(), @@ -45,36 +58,374 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { - const sampleParams = sampleSignalAlertParams(undefined); - const fakeSignalSourceHit = buildBulkBody(sampleDocNoSortId, sampleParams, sampleSignalId); + const sampleParams = sampleRuleAlertParams(); + const fakeSignalSourceHit = buildBulkBody({ + doc: sampleDocNoSortId(), + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', - '@timestamp': 'someTimeStamp', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, signal: { - id: sampleSignalId, - '@timestamp': fakeSignalSourceHit.signal['@timestamp'], // timestamp generated in the body - rule_revision: 1, - rule_id: sampleParams.ruleId, - rule_type: sampleParams.type, + original_event: { + action: 'socket_opened', + dataset: 'socket', + module: 'system', + }, parent: { - id: sampleDocNoSortId._id, + id: sampleIdGuid, type: 'event', - index: sampleDocNoSortId._index, + index: 'myFakeSignalIndex', depth: 1, }, - name: sampleParams.name, - severity: sampleParams.severity, - description: sampleParams.description, - original_time: sampleDocNoSortId._source['@timestamp'], - index_patterns: sampleParams.index, - references: sampleParams.references, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + original_event: { + kind: 'event', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, }, }); }); }); - describe('singleBulkIndex', () => { - test('create successful bulk index', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + describe('singleBulkCreate', () => { + describe('create signal id gereateId', () => { + test('two docs with same index, id, and version should have same id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId); + expect(firstHash).toEqual(generatedHash); + expect(secondHash).toEqual(generatedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + }); + test('two docs with different index, id, and version should have different id', () => { + const findex = 'myfakeindex'; + const findex2 = 'mysecondfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'mysecondfakeindexsomefakeid1rule-1' + const secondGeneratedHash = + 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex2, fid, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, different id, and same version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const fid2 = 'somefakeid2'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'myfakeindexsomefakeid21rule-1' + const secondGeneratedHash = + '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid2, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, same id, and different version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const version2 = '2'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid2rule-1' + const secondGeneratedHash = + 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version2, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { + const longIndexName = + 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const firstHash = generateId(longIndexName, fid, version, ruleId); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + }); + test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const ruleId2 = 'rule-2'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid1rule-2' + const secondGeneratedHash = + '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId2); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + }); + test('create successful bulk create', async () => { + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -85,94 +436,156 @@ describe('utils', () => { }, ], }); - const successfulSingleBulkIndex = await singleBulkIndex( - sampleSearchResult, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); - expect(successfulSingleBulkIndex).toEqual(true); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + test('create successful bulk create with docs with no versioning', async () => { + const sampleParams = sampleRuleAlertParams(); + const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create unsuccessful bulk create due to empty search results', async () => { + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); - const successfulSingleBulkIndex = await singleBulkIndex( - sampleSearchResult, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); - expect(successfulSingleBulkIndex).toEqual(true); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to bulk index errors', async () => { - // need a sample search result, sample signal params, mock service, mock logger - const sampleParams = sampleSignalAlertParams(undefined); + test('create successful bulk create when bulk create has errors', async () => { + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, - }); - const successfulSingleBulkIndex = await singleBulkIndex( - sampleSearchResult, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); expect(mockLogger.error).toHaveBeenCalled(); - expect(successfulSingleBulkIndex).toEqual(false); + expect(successfulsingleBulkCreate).toEqual(true); }); }); describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( - singleSearchAfter(searchAfterSortId, sampleParams, mockService, mockLogger) + singleSearchAfter({ + searchAfterSortId, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + }) ).rejects.toThrow('Attempted to search after with empty sort id'); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); - const searchAfterResult = await singleSearchAfter( + const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - sampleParams, - mockService, - mockLogger - ); + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + }); expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( - singleSearchAfter(searchAfterSortId, sampleParams, mockService, mockLogger) + singleSearchAfter({ + searchAfterSortId, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + }) ).rejects.toThrow('Fake Error'); }); }); - describe('searchAfterAndBulkIndex', () => { + describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); - const result = await searchAfterAndBulkIndex( - sampleEmptyDocSearchResults, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + const sampleParams = sampleRuleAlertParams(); + const result = await searchAfterAndBulkCreate({ + someResult: sampleEmptyDocSearchResults, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(30); + const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -183,7 +596,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) .mockReturnValueOnce({ took: 100, errors: false, @@ -193,7 +606,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) .mockReturnValueOnce({ took: 100, errors: false, @@ -203,35 +616,46 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex( - repeatedSearchResultsWithSortId(4), - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); }); - test('if unsuccessful first bulk index', async () => { - const sampleParams = sampleSignalAlertParams(10); - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, // will cause singleBulkIndex to return false - }); - const result = await searchAfterAndBulkIndex( - repeatedSearchResultsWithSortId(4), - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + test('if unsuccessful first bulk create', async () => { + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + const sampleParams = sampleRuleAlertParams(10); + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids', async () => { - const sampleParams = sampleSignalAlertParams(undefined); - + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -241,18 +665,25 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex( - sampleDocSearchResultsNoSortId, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -262,17 +693,25 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex( - sampleDocSearchResultsNoSortIdNoHits, - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortIdNoHits(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -283,21 +722,28 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId); - const result = await searchAfterAndBulkIndex( - repeatedSearchResultsWithSortId(4), - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(result).toEqual(true); }); - test('if logs error when iteration is unsuccessful when bulk index results in a failure', async () => { - const sampleParams = sampleSignalAlertParams(5); + test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ - // first bulk insert took: 100, errors: false, items: [ @@ -306,19 +752,26 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsWithSortId); // get some more docs - const result = await searchAfterAndBulkIndex( - repeatedSearchResultsWithSortId(4), - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); - expect(mockLogger.error).toHaveBeenCalled(); + .mockReturnValueOnce(sampleEmptyDocSearchResults); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(result).toEqual(true); }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -329,15 +782,296 @@ describe('utils', () => { }, ], }) - .mockRejectedValueOnce(Error('Fake Error')); - const result = await searchAfterAndBulkIndex( - repeatedSearchResultsWithSortId(4), - sampleParams, - mockService, - mockLogger, - sampleSignalId - ); + .mockImplementation(() => { + throw Error('Fake Error'); + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + }); expect(result).toEqual(false); }); }); + + describe('buildEventTypeSignal', () => { + test('it returns the event appended of kind signal if it does not exist', () => { + const doc = sampleDocNoSortId(); + delete doc._source.event; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event appended of kind signal if it is an empty object', () => { + const doc = sampleDocNoSortId(); + doc._source.event = {}; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event with kind signal and other properties if they exist', () => { + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const eventType = buildEventTypeSignal(doc); + const expected: object = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'signal', + }; + expect(eventType).toEqual(expected); + }); + }); + + describe('buildSignal', () => { + test('it builds a signal as expected without original_event if event does not exist', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + delete doc._source.event; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + + test('it builds a signal as expected with original_event if is present', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + doc._source.event = { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + }); + + describe('buildRule', () => { + test('it builds a rule as expected with filters present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ]; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: false, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + filters: [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ], + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if enabled is null if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if filters is undefined if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 2967f41ffb697..c3988b8fea458 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -3,67 +3,205 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { performance } from 'perf_hooks'; -import { SignalHit } from '../../types'; -import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { pickBy } from 'lodash/fp'; +import { SignalHit, Signal } from '../../types'; import { Logger } from '../../../../../../../../src/core/server'; import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalSourceHit, SignalSearchResponse, SignalAlertParams, BulkResponse } from './types'; +import { + SignalSourceHit, + SignalSearchResponse, + BulkResponse, + RuleTypeParams, + OutputRuleAlertRest, +} from './types'; import { buildEventsSearchQuery } from './build_events_query'; +interface BuildRuleParams { + ruleParams: RuleTypeParams; + name: string; + id: string; + enabled: boolean; + createdBy: string; + updatedBy: string; + interval: string; +} + +export const buildRule = ({ + ruleParams, + name, + id, + enabled, + createdBy, + updatedBy, + interval, +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { + id, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + filter: ruleParams.filter, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, + interval, + language: ruleParams.language, + name, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags: ruleParams.tags, + type: ruleParams.type, + to: ruleParams.to, + enabled, + filters: ruleParams.filters, + created_by: createdBy, + updated_by: updatedBy, + }); +}; + +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { + const signal: Signal = { + parent: { + id: doc._id, + type: 'event', + index: doc._index, + depth: 1, + }, + original_time: doc._source['@timestamp'], + status: 'open', + rule, + }; + if (doc._source.event != null) { + return { ...signal, original_event: doc._source.event }; + } + return signal; +}; + +interface BuildBulkBodyParams { + doc: SignalSourceHit; + ruleParams: RuleTypeParams; + id: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; +} + +export const buildEventTypeSignal = (doc: SignalSourceHit): object => { + if (doc._source.event != null && doc._source.event instanceof Object) { + return { ...doc._source.event, kind: 'signal' }; + } else { + return { kind: 'signal' }; + } +}; + // format search_after result for signals index. -export const buildBulkBody = ( - doc: SignalSourceHit, - signalParams: SignalAlertParams, - id: string -): SignalHit => { - return { +export const buildBulkBody = ({ + doc, + ruleParams, + id, + name, + createdBy, + updatedBy, + interval, + enabled, +}: BuildBulkBodyParams): SignalHit => { + const rule = buildRule({ + ruleParams, + id, + name, + enabled, + createdBy, + updatedBy, + interval, + }); + const signal = buildSignal(doc, rule); + const event = buildEventTypeSignal(doc); + const signalHit: SignalHit = { ...doc._source, - signal: { - '@timestamp': new Date().toISOString(), - id, - rule_revision: 1, - rule_id: signalParams.ruleId, - rule_type: signalParams.type, - parent: { - id: doc._id, - type: 'event', - index: doc._index, - depth: 1, - }, - name: signalParams.name, - severity: signalParams.severity, - description: signalParams.description, - original_time: doc._source['@timestamp'], - index_patterns: signalParams.index, - references: signalParams.references, - }, + '@timestamp': new Date().toISOString(), + event, + signal, }; + return signalHit; }; +interface SingleBulkCreateParams { + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; +} + +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); + // Bulk Index documents. -export const singleBulkIndex = async ( - sr: SignalSearchResponse, - params: SignalAlertParams, - service: AlertServices, - logger: Logger, - id: string -): Promise => { - if (sr.hits.hits.length === 0) { +export const singleBulkCreate = async ({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, +}: SingleBulkCreateParams): Promise => { + if (someResult.hits.hits.length === 0) { return true; } - const bulkBody = sr.hits.hits.flatMap(doc => [ + // index documents after creating an ID based on the + // source documents' originating index, and the original + // document _id. This will allow two documents from two + // different indexes with the same ID to be + // indexed, and prevents us from creating any updates + // to the documents once inserted into the signals index, + // while preventing duplicates from being added to the + // signals index if rules are re-run over the same time + // span. Also allow for versioning. + const bulkBody = someResult.hits.hits.flatMap(doc => [ { - index: { - _index: process.env.SIGNALS_INDEX || DEFAULT_SIGNALS_INDEX, - _id: doc._id, + create: { + _index: signalsIndex, + _id: generateId( + doc._index, + doc._id, + doc._version ? doc._version.toString() : '', + ruleParams.ruleId ?? '' + ), }, }, - buildBulkBody(doc, params, id), + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled }), ]); const time1 = performance.now(); - const firstResult: BulkResponse = await service.callCluster('bulk', { - index: process.env.SIGNALS_INDEX || DEFAULT_SIGNALS_INDEX, + const firstResult: BulkResponse = await services.callCluster('bulk', { + index: signalsIndex, refresh: false, body: bulkBody, }); @@ -71,32 +209,60 @@ export const singleBulkIndex = async ( logger.debug(`individual bulk process time took: ${time2 - time1} milliseconds`); logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); if (firstResult.errors) { - logger.error(`[-] bulkResponse had errors: ${JSON.stringify(firstResult.errors, null, 2)}`); - return false; + // go through the response status errors and see what + // types of errors they are, count them up, and log them. + const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { + if (item.create.error) { + const responseStatusKey = item.create.status.toString(); + acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; + } + return acc; + }, {}); + /* + the logging output below should look like + {'409': 55} + which is read as "there were 55 counts of 409 errors returned from bulk create" + */ + logger.error( + `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( + errorCountMap, + null, + 2 + )}` + ); } return true; }; +interface SingleSearchAfterParams { + searchAfterSortId: string | undefined; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + pageSize: number; +} + // utilize search_after for paging results into bulk. -export const singleSearchAfter = async ( - searchAfterSortId: string | undefined, - params: SignalAlertParams, - service: AlertServices, - logger: Logger -): Promise => { +export const singleSearchAfter = async ({ + searchAfterSortId, + ruleParams, + services, + logger, + pageSize, +}: SingleSearchAfterParams): Promise => { if (searchAfterSortId == null) { throw Error('Attempted to search after with empty sort id'); } try { const searchAfterQuery = buildEventsSearchQuery({ - index: params.index, - from: params.from, - to: params.to, - filter: params.filter, - size: params.size ? params.size : 1000, + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter: ruleParams.filter, + size: pageSize, searchAfterSortId, }); - const nextSearchAfterResult: SignalSearchResponse = await service.callCluster( + const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( 'search', searchAfterQuery ); @@ -107,32 +273,62 @@ export const singleSearchAfter = async ( } }; +interface SearchAfterAndBulkCreateParams { + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; + pageSize: number; +} + // search_after through documents and re-index using bulk endpoint. -export const searchAfterAndBulkIndex = async ( - someResult: SignalSearchResponse, - params: SignalAlertParams, - service: AlertServices, - logger: Logger, - id: string -): Promise => { +export const searchAfterAndBulkCreate = async ({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + pageSize, +}: SearchAfterAndBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } logger.debug('[+] starting bulk insertion'); - const firstBulkIndexSuccess = await singleBulkIndex(someResult, params, service, logger, id); - if (!firstBulkIndexSuccess) { - logger.error('First bulk index was unsuccessful'); - return false; - } - + await singleBulkCreate({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + }); const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; // maxTotalHitsSize represents the total number of docs to - // query for. If maxSignals is present we will only query - // up to max signals - otherwise use the value - // from track_total_hits. - const maxTotalHitsSize = params.maxSignals ? params.maxSignals : totalHits; + // query for, no matter the size of each individual page of search results. + // If the total number of hits for the overall search result is greater than + // maxSignals, default to requesting a total of maxSignals, otherwise use the + // totalHits in the response from the searchAfter query. + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -151,13 +347,16 @@ export const searchAfterAndBulkIndex = async ( while (hitsSize < maxTotalHitsSize && hitsSize !== 0) { try { logger.debug(`sortIds: ${sortIds}`); - const searchAfterResult: SignalSearchResponse = await singleSearchAfter( - sortId, - params, - service, - logger - ); - sortIds = searchAfterResult.hits.hits[0].sort; + const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ + searchAfterSortId: sortId, + ruleParams, + services, + logger, + pageSize, // maximum number of docs to receive per search result. + }); + if (searchAfterResult.hits.hits.length === 0) { + return true; + } hitsSize += searchAfterResult.hits.hits.length; logger.debug(`size adjusted: ${hitsSize}`); sortIds = searchAfterResult.hits.hits[0].sort; @@ -167,16 +366,25 @@ export const searchAfterAndBulkIndex = async ( } sortId = sortIds[0]; logger.debug('next bulk index'); - const bulkSuccess = await singleBulkIndex(searchAfterResult, params, service, logger, id); + await singleBulkCreate({ + someResult: searchAfterResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + }); logger.debug('finished next bulk index'); - if (!bulkSuccess) { - logger.error('[-] bulk index failed but continuing'); - } } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); return false; } } - logger.debug(`[+] completed bulk index of ${totalHits}`); + logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); return true; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c73159de85c10..4c49326fbb32a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,18 +6,20 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. -export const typicalPayload = (): Partial> => ({ +export const typicalPayload = (): Partial> => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + risk_score: 50, type: 'query', from: 'now-6m', to: 'now', @@ -26,12 +28,13 @@ export const typicalPayload = (): Partial> language: 'kuery', }); -export const typicalFilterPayload = (): Partial => ({ +export const typicalFilterPayload = (): Partial => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', name: 'Detect Root/Admin Users', + risk_score: 50, type: 'filter', from: 'now-6m', to: 'now', @@ -61,7 +64,7 @@ interface FindHit { page: number; perPage: number; total: number; - data: SignalAlertType[]; + data: RuleAlertType[]; } export const getFindResult = (): FindHit => ({ @@ -78,7 +81,7 @@ export const getFindResultWithSingleHit = (): FindHit => ({ data: [getResult()], }); -export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ +export const getFindResultWithMultiHits = (data: RuleAlertType[]): FindHit => ({ page: 1, perPage: 1, total: 2, @@ -110,23 +113,26 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const getResult = (): SignalAlertType => ({ +export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], alertTypeId: 'siem.signals', - alertTypeParams: { + params: { description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], falsePositives: [], from: 'now-6m', - filter: undefined, + filter: null, immutable: false, query: 'user.name: root or user.name: admin', language: 'kuery', - savedId: undefined, - filters: undefined, + outputIndex: '.siem-signals', + savedId: null, + meta: null, + filters: null, + riskScore: 50, maxSignals: 100, size: 1, severity: 'high', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts new file mode 100644 index 0000000000000..4c222c196300c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from './__mocks__/_mock_server'; +import { createRulesRoute } from './create_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + createActionResult, + getCreateRequest, + typicalPayload, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; + +describe('create_rules', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient } = createMockServer()); + createRulesRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getCreateRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + createRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + createRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + createRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + // missing rule_id should return 200 as it will be auto generated if not given + const { rule_id, ...noRuleId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_RULES_URL, + payload: noRuleId, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'query', + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if type is filter', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + // Cannot type request with a ServerInjectOptions as the type system complains + // about the property filter involving Hapi types, so I left it off for now + const { language, query, type, ...noType } = typicalPayload(); + const request = { + method: 'POST', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'filter', + filter: {}, + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'something-made-up', + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts new file mode 100644 index 0000000000000..7e1ac07e1f0aa --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import Boom from 'boom'; +import uuid from 'uuid'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { createRules } from '../alerts/create_rules'; +import { RulesRequest } from '../alerts/types'; +import { createRulesSchema } from './schemas'; +import { ServerFacade } from '../../../types'; +import { readRules } from '../alerts/read_rules'; +import { transformOrError } from './utils'; + +export const createCreateRulesRoute: Hapi.ServerRoute = { + method: 'POST', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:signals-all'], + validate: { + options: { + abortEarly: false, + }, + payload: createRulesSchema, + }, + }, + async handler(request: RulesRequest, headers) { + const { + description, + enabled, + false_positives: falsePositives, + filter, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + references, + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); + } + } + + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + ruleId: ruleId != null ? ruleId : uuid.v4(), + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + references, + }); + return transformOrError(createdRule); + }, +}; + +export const createRulesRoute = (server: ServerFacade) => { + server.route(createCreateRulesRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts deleted file mode 100644 index 1232fe3ce219d..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ /dev/null @@ -1,141 +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 { - createMockServer, - createMockServerWithoutActionClientDecoration, - createMockServerWithoutAlertClientDecoration, - createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; -import { createSignalsRoute } from './create_signals_route'; -import { ServerInjectOptions } from 'hapi'; -import { - getFindResult, - getResult, - createActionResult, - getCreateRequest, - typicalPayload, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; - -describe('create_signals', () => { - let { server, alertsClient, actionsClient } = createMockServer(); - - beforeEach(() => { - jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - createSignalsRoute(server); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getCreateRequest()); - expect(statusCode).toBe(200); - }); - - test('returns 404 if actionClient is not available on the route', async () => { - const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createSignalsRoute(serverWithoutActionClient); - const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createSignalsRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient and actionClient are both not available on the route', async () => { - const { - serverWithoutActionOrAlertClient, - } = createMockServerWithoutActionOrAlertClientDecoration(); - createSignalsRoute(serverWithoutActionOrAlertClient); - const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); - expect(statusCode).toBe(404); - }); - }); - - describe('validation', () => { - test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - // missing rule_id should return 200 as it will be auto generated if not given - const { rule_id, ...noRuleId } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'POST', - url: DETECTION_ENGINE_RULES_URL, - payload: noRuleId, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - - test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - const { type, ...noType } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'POST', - url: DETECTION_ENGINE_RULES_URL, - payload: { - ...noType, - type: 'query', - }, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - - test('returns 200 if type is filter', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - // Cannot type request with a ServerInjectOptions as the type system complains - // about the property filter involving Hapi types, so I left it off for now - const { language, query, type, ...noType } = typicalPayload(); - const request = { - method: 'POST', - url: DETECTION_ENGINE_RULES_URL, - payload: { - ...noType, - type: 'filter', - filter: {}, - }, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - - test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - const { type, ...noType } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'POST', - url: DETECTION_ENGINE_RULES_URL, - payload: { - ...noType, - type: 'something-made-up', - }, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts deleted file mode 100644 index ce386cacccf9a..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; -import Boom from 'boom'; -import uuid from 'uuid'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createSignals } from '../alerts/create_signals'; -import { SignalsRequest } from '../alerts/types'; -import { createSignalsSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { readSignals } from '../alerts/read_signals'; -import { transformOrError } from './utils'; - -export const createCreateSignalsRoute: Hapi.ServerRoute = { - method: 'POST', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:signals-all'], - validate: { - options: { - abortEarly: false, - }, - payload: createSignalsSchema, - }, - }, - async handler(request: SignalsRequest, headers) { - const { - description, - enabled, - // eslint-disable-next-line @typescript-eslint/camelcase - false_positives: falsePositives, - filter, - from, - immutable, - query, - language, - // eslint-disable-next-line @typescript-eslint/camelcase - saved_id: savedId, - filters, - // eslint-disable-next-line @typescript-eslint/camelcase - rule_id: ruleId, - index, - interval, - // eslint-disable-next-line @typescript-eslint/camelcase - max_signals: maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - } = request.payload; - - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - - if (ruleId != null) { - const signal = await readSignals({ alertsClient, ruleId }); - if (signal != null) { - return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); - } - } - const createdSignal = await createSignals({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - savedId, - filters, - ruleId: ruleId != null ? ruleId : uuid.v4(), - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - }); - return transformOrError(createdSignal); - }, -}; - -export const createSignalsRoute = (server: ServerFacade) => { - server.route(createCreateSignalsRoute); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts new file mode 100644 index 0000000000000..0808051964dc1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from './__mocks__/_mock_server'; + +import { deleteRulesRoute } from './delete_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + getDeleteRequest, + getFindResultWithSingleHit, + getDeleteRequestById, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; + +describe('delete_rules', () => { + let { server, alertsClient } = createMockServer(); + + beforeEach(() => { + ({ server, alertsClient } = createMockServer()); + deleteRulesRoute(server); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteRequestById()); + expect(statusCode).toBe(200); + }); + + test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + deleteRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + deleteRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + deleteRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if given a non-existent id', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const request: ServerInjectOptions = { + method: 'DELETE', + url: DETECTION_ENGINE_RULES_URL, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts new file mode 100644 index 0000000000000..12dff0dd60c14 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { deleteRules } from '../alerts/delete_rules'; +import { ServerFacade } from '../../../types'; +import { queryRulesSchema } from './schemas'; +import { QueryRequest } from '../alerts/types'; +import { getIdError, transformOrError } from './utils'; + +export const createDeleteRulesRoute: Hapi.ServerRoute = { + method: 'DELETE', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:signals-all'], + validate: { + options: { + abortEarly: false, + }, + query: queryRulesSchema, + }, + }, + async handler(request: QueryRequest, headers) { + const { id, rule_id: ruleId } = request.query; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (alertsClient == null || actionsClient == null) { + return headers.response().code(404); + } + + const rule = await deleteRules({ + actionsClient, + alertsClient, + id, + ruleId, + }); + + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + }, +}; + +export const deleteRulesRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts deleted file mode 100644 index 95816aa55d1fe..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ /dev/null @@ -1,99 +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 { - createMockServer, - createMockServerWithoutActionClientDecoration, - createMockServerWithoutAlertClientDecoration, - createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; - -import { deleteSignalsRoute } from './delete_signals_route'; -import { ServerInjectOptions } from 'hapi'; -import { - getFindResult, - getResult, - getDeleteRequest, - getFindResultWithSingleHit, - getDeleteRequestById, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; - -describe('delete_signals', () => { - let { server, alertsClient } = createMockServer(); - - beforeEach(() => { - ({ server, alertsClient } = createMockServer()); - deleteSignalsRoute(server); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - const { statusCode } = await server.inject(getDeleteRequest()); - expect(statusCode).toBe(200); - }); - - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - const { statusCode } = await server.inject(getDeleteRequestById()); - expect(statusCode).toBe(200); - }); - - test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - const { statusCode } = await server.inject(getDeleteRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if actionClient is not available on the route', async () => { - const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteSignalsRoute(serverWithoutActionClient); - const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteSignalsRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient and actionClient are both not available on the route', async () => { - const { - serverWithoutActionOrAlertClient, - } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteSignalsRoute(serverWithoutActionOrAlertClient); - const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); - expect(statusCode).toBe(404); - }); - }); - - describe('validation', () => { - test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - const request: ServerInjectOptions = { - method: 'DELETE', - url: DETECTION_ENGINE_RULES_URL, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts deleted file mode 100644 index 1f5494a54ddca..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; - -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteSignals } from '../alerts/delete_signals'; -import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; -import { QueryRequest } from '../alerts/types'; -import { getIdError, transformOrError } from './utils'; - -export const createDeleteSignalsRoute: Hapi.ServerRoute = { - method: 'DELETE', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:signals-all'], - validate: { - options: { - abortEarly: false, - }, - query: querySignalSchema, - }, - }, - async handler(request: QueryRequest, headers) { - const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (alertsClient == null || actionsClient == null) { - return headers.response().code(404); - } - - const signal = await deleteSignals({ - actionsClient, - alertsClient, - id, - ruleId, - }); - - if (signal != null) { - return transformOrError(signal); - } else { - return getIdError({ id, ruleId }); - } - }, -}; - -export const deleteSignalsRoute = (server: ServerFacade): void => { - server.route(createDeleteSignalsRoute); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts new file mode 100644 index 0000000000000..dae40f05155dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from './__mocks__/_mock_server'; + +import { findRulesRoute } from './find_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; + +describe('find_rules', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + ({ server, alertsClient, actionsClient } = createMockServer()); + findRulesRoute(server); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.find.mockResolvedValue({ + page: 1, + perPage: 1, + total: 0, + data: [], + }); + alertsClient.get.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getFindRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + findRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + findRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + findRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if a bad query parameter is given', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 200 if the set of optional query parameters are given', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts new file mode 100644 index 0000000000000..893fb3f689d16 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -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 Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { findRules } from '../alerts/find_rules'; +import { FindRulesRequest } from '../alerts/types'; +import { findRulesSchema } from './schemas'; +import { ServerFacade } from '../../../types'; +import { transformFindAlertsOrError } from './utils'; + +export const createFindRulesRoute: Hapi.ServerRoute = { + method: 'GET', + path: `${DETECTION_ENGINE_RULES_URL}/_find`, + options: { + tags: ['access:signals-all'], + validate: { + options: { + abortEarly: false, + }, + query: findRulesSchema, + }, + }, + async handler(request: FindRulesRequest, headers) { + const { query } = request; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + const rules = await findRules({ + alertsClient, + perPage: query.per_page, + page: query.page, + sortField: query.sort_field, + sortOrder: query.sort_order, + filter: query.filter, + }); + return transformFindAlertsOrError(rules); + }, +}; + +export const findRulesRoute = (server: ServerFacade) => { + server.route(createFindRulesRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts deleted file mode 100644 index be3dce36e8716..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ /dev/null @@ -1,92 +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 { - createMockServer, - createMockServerWithoutActionClientDecoration, - createMockServerWithoutAlertClientDecoration, - createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; - -import { findSignalsRoute } from './find_signals_route'; -import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; - -describe('find_signals', () => { - let { server, alertsClient, actionsClient } = createMockServer(); - - beforeEach(() => { - ({ server, alertsClient, actionsClient } = createMockServer()); - findSignalsRoute(server); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.find.mockResolvedValue({ - page: 1, - perPage: 1, - total: 0, - data: [], - }); - alertsClient.get.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getFindRequest()); - expect(statusCode).toBe(200); - }); - - test('returns 404 if actionClient is not available on the route', async () => { - const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findSignalsRoute(serverWithoutActionClient); - const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findSignalsRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient and actionClient are both not available on the route', async () => { - const { - serverWithoutActionOrAlertClient, - } = createMockServerWithoutActionOrAlertClientDecoration(); - findSignalsRoute(serverWithoutActionOrAlertClient); - const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); - expect(statusCode).toBe(404); - }); - }); - - describe('validation', () => { - test('returns 400 if a bad query parameter is given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - const request: ServerInjectOptions = { - method: 'GET', - url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - - test('returns 200 if the set of optional query parameters are given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - const request: ServerInjectOptions = { - method: 'GET', - url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts deleted file mode 100644 index 18252c4f27fb0..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findSignals } from '../alerts/find_signals'; -import { FindSignalsRequest } from '../alerts/types'; -import { findSignalsSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { transformFindAlertsOrError } from './utils'; - -export const createFindSignalRoute: Hapi.ServerRoute = { - method: 'GET', - path: `${DETECTION_ENGINE_RULES_URL}/_find`, - options: { - tags: ['access:signals-all'], - validate: { - options: { - abortEarly: false, - }, - query: findSignalsSchema, - }, - }, - async handler(request: FindSignalsRequest, headers) { - const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - - const signals = await findSignals({ - alertsClient, - perPage: query.per_page, - page: query.page, - sortField: query.sort_field, - }); - return transformFindAlertsOrError(signals); - }, -}; - -export const findSignalsRoute = (server: ServerFacade) => { - server.route(createFindSignalRoute); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts new file mode 100644 index 0000000000000..47ecf62f41be9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from './__mocks__/_mock_server'; + +import { readRulesRoute } from './read_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + getReadRequest, + getFindResultWithSingleHit, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; + +describe('read_signals', () => { + let { server, alertsClient } = createMockServer(); + + beforeEach(() => { + ({ server, alertsClient } = createMockServer()); + readRulesRoute(server); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getReadRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + readRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + readRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + readRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if given a non-existent id', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const request: ServerInjectOptions = { + method: 'GET', + url: DETECTION_ENGINE_RULES_URL, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts new file mode 100644 index 0000000000000..4642c34fbe339 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { getIdError, transformOrError } from './utils'; + +import { readRules } from '../alerts/read_rules'; +import { ServerFacade } from '../../../types'; +import { queryRulesSchema } from './schemas'; +import { QueryRequest } from '../alerts/types'; + +export const createReadRulesRoute: Hapi.ServerRoute = { + method: 'GET', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:signals-all'], + validate: { + options: { + abortEarly: false, + }, + query: queryRulesSchema, + }, + }, + async handler(request: QueryRequest, headers) { + const { id, rule_id: ruleId } = request.query; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + const rule = await readRules({ + alertsClient, + id, + ruleId, + }); + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + }, +}; + +export const readRulesRoute = (server: ServerFacade) => { + server.route(createReadRulesRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts deleted file mode 100644 index 021bcc7b8b48e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ /dev/null @@ -1,81 +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 { - createMockServer, - createMockServerWithoutActionClientDecoration, - createMockServerWithoutAlertClientDecoration, - createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; - -import { readSignalsRoute } from './read_signals_route'; -import { ServerInjectOptions } from 'hapi'; -import { - getFindResult, - getResult, - getReadRequest, - getFindResultWithSingleHit, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; - -describe('read_signals', () => { - let { server, alertsClient } = createMockServer(); - - beforeEach(() => { - ({ server, alertsClient } = createMockServer()); - readSignalsRoute(server); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getReadRequest()); - expect(statusCode).toBe(200); - }); - - test('returns 404 if actionClient is not available on the route', async () => { - const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readSignalsRoute(serverWithoutActionClient); - const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readSignalsRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient and actionClient are both not available on the route', async () => { - const { - serverWithoutActionOrAlertClient, - } = createMockServerWithoutActionOrAlertClientDecoration(); - readSignalsRoute(serverWithoutActionOrAlertClient); - const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); - expect(statusCode).toBe(404); - }); - }); - - describe('validation', () => { - test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - const request: ServerInjectOptions = { - method: 'GET', - url: DETECTION_ENGINE_RULES_URL, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts deleted file mode 100644 index 2d662f9049cce..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { getIdError, transformOrError } from './utils'; - -import { readSignals } from '../alerts/read_signals'; -import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; -import { QueryRequest } from '../alerts/types'; - -export const createReadSignalsRoute: Hapi.ServerRoute = { - method: 'GET', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:signals-all'], - validate: { - options: { - abortEarly: false, - }, - query: querySignalSchema, - }, - }, - async handler(request: QueryRequest, headers) { - const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - const signal = await readSignals({ - alertsClient, - id, - ruleId, - }); - if (signal != null) { - return transformOrError(signal); - } else { - return getIdError({ id, ruleId }); - } - }, -}; - -export const readSignalsRoute = (server: ServerFacade) => { - server.route(createReadSignalsRoute); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index ecb42399932f6..6c7e5c4054326 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createSignalsSchema, - updateSignalSchema, - findSignalsSchema, - querySignalSchema, -} from './schemas'; -import { - SignalAlertParamsRest, - FindParamsRest, - UpdateSignalAlertParamsRest, -} from '../alerts/types'; +import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; describe('schemas', () => { - describe('create signals schema', () => { + describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -32,7 +23,7 @@ describe('schemas', () => { test('[rule_id] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -40,7 +31,7 @@ describe('schemas', () => { test('[rule_id, description] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -49,7 +40,7 @@ describe('schemas', () => { test('[rule_id, description, from] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -59,7 +50,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -70,7 +61,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -82,7 +73,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +86,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -109,7 +100,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -124,7 +115,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -140,8 +131,9 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -155,10 +147,49 @@ describe('schemas', () => { ).toBeTruthy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -173,10 +204,30 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + risk_score: 50, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -192,8 +243,10 @@ describe('schemas', () => { test('If filter type is set then filter is required', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -208,8 +261,10 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -226,8 +281,10 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -244,8 +301,10 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -262,8 +321,10 @@ describe('schemas', () => { test('allows references to be sent as valid', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -281,8 +342,10 @@ describe('schemas', () => { test('defaults references to an array', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -299,10 +362,12 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { references: number[] } + createRulesSchema.validate< + Partial> & { references: number[] } >({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -320,10 +385,12 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { index: number[] } + createRulesSchema.validate< + Partial> & { index: number[] } >({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -340,8 +407,10 @@ describe('schemas', () => { test('defaults interval to 5 min', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -355,8 +424,10 @@ describe('schemas', () => { test('defaults max signals to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -371,8 +442,10 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -389,8 +462,10 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -405,8 +480,10 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -420,10 +497,12 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('saved_query type cannot have filters with it', () => { + test('saved_query type can have filters with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -435,13 +514,37 @@ describe('schemas', () => { saved_id: 'some id', filters: [], }).error + ).toBeFalsy(); + }); + + test('filters cannot be a string', () => { + expect( + createRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: 'some string', + }).error ).toBeTruthy(); }); test('saved_query type cannot have filter with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -458,8 +561,10 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -477,8 +582,10 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -496,8 +603,10 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -515,8 +624,10 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -535,8 +646,10 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -555,8 +668,10 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -575,8 +690,10 @@ describe('schemas', () => { test('You can optionally send in an array of tags', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -596,11 +713,62 @@ describe('schemas', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { tags: number[] } + createRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error + ).toBeTruthy(); + }); + + test('You can optionally send in an array of false positives', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: ['false_1', 'false_2'], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of false positives that are numbers', () => { + expect( + createRulesSchema.validate< + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', + false_positives: [5, 4], from: 'now-5m', to: 'now', index: ['index-1'], @@ -612,19 +780,20 @@ describe('schemas', () => { query: 'some query', language: 'kuery', max_signals: 1, - tags: [0, 1, 2], }).error ).toBeTruthy(); }); - test('You can optionally send in an array of false positives', () => { + test('You can optionally set the immutable to be true', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', - false_positives: ['false_1', 'false_2'], from: 'now-5m', to: 'now', + immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -638,16 +807,18 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('You cannot send in an array of false positives that are numbers', () => { + test('You cannot set the immutable to be a number', () => { expect( - createSignalsSchema.validate< - Partial> & { false_positives: number[] } + createRulesSchema.validate< + Partial> & { immutable: number } >({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', - false_positives: [5, 4], from: 'now-5m', to: 'now', + immutable: 5, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -661,10 +832,58 @@ describe('schemas', () => { ).toBeTruthy(); }); - test('You can optionally set the immutable to be true', () => { + test('You cannot set the risk_score to 101', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 101, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You cannot set the risk_score to -1', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: -1, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You can set the risk_score to 0', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 0, description: 'some description', from: 'now-5m', to: 'now', @@ -682,16 +901,16 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('You cannot set the immutable to be a number', () => { + test('You can set the risk_score to 100', () => { expect( - createSignalsSchema.validate< - Partial> & { immutable: number } - >({ + createRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 100, description: 'some description', from: 'now-5m', to: 'now', - immutable: 5, + immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -702,20 +921,138 @@ describe('schemas', () => { language: 'kuery', max_signals: 1, }).error + ).toBeFalsy(); + }); + + test('You can set meta to any object you want', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }).error + ).toBeFalsy(); + }); + + test('You cannot create meta as a string', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: 'should not work', + }).error ).toBeTruthy(); }); + + test('You can have an empty query string when filters are present', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: '', + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can omit the query string when filters are present', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('query string defaults to empty string when present with filters', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).value.query + ).toEqual(''); + }); }); - describe('update signals schema', () => { + describe('update rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect( - updateSignalSchema.validate>({}).error - ).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -723,7 +1060,7 @@ describe('schemas', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); @@ -731,7 +1068,7 @@ describe('schemas', () => { test('[rule_id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeFalsy(); @@ -739,7 +1076,7 @@ describe('schemas', () => { test('[id and rule_id] does not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'id-1', rule_id: 'rule-1', }).error @@ -748,7 +1085,7 @@ describe('schemas', () => { test('[rule_id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -757,16 +1094,25 @@ describe('schemas', () => { test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', }).error ).toBeFalsy(); }); + test('[id, risk_score] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + risk_score: 10, + }).error + ).toBeFalsy(); + }); + test('[rule_id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -776,7 +1122,7 @@ describe('schemas', () => { test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -786,7 +1132,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -797,7 +1143,7 @@ describe('schemas', () => { test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -808,7 +1154,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -820,7 +1166,7 @@ describe('schemas', () => { test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -832,7 +1178,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -845,7 +1191,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -858,7 +1204,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -872,7 +1218,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -886,7 +1232,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -901,7 +1247,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -916,7 +1262,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -932,7 +1278,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -948,7 +1294,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -965,7 +1311,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -982,7 +1328,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1000,7 +1346,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1018,7 +1364,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1035,7 +1381,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1052,7 +1398,7 @@ describe('schemas', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1068,7 +1414,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1086,7 +1432,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1104,7 +1450,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1122,7 +1468,7 @@ describe('schemas', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1141,7 +1487,7 @@ describe('schemas', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1159,7 +1505,7 @@ describe('schemas', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1174,7 +1520,7 @@ describe('schemas', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1190,8 +1536,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { references: number[] } + updateRulesSchema.validate< + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1211,8 +1557,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { index: number[] } + updateRulesSchema.validate< + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1231,7 +1577,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1249,7 +1595,7 @@ describe('schemas', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1265,7 +1611,7 @@ describe('schemas', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1280,9 +1626,9 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('saved_query type cannot have filters with it', () => { + test('saved_query type can have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1295,12 +1641,12 @@ describe('schemas', () => { saved_id: 'some id', filters: [], }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1318,7 +1664,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1337,7 +1683,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1356,7 +1702,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1375,7 +1721,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1395,7 +1741,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1415,7 +1761,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1432,27 +1778,61 @@ describe('schemas', () => { }).error ).toBeFalsy(); }); + + test('meta can be updated', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + meta: { whateverYouWant: 'anything_at_all' }, + }).error + ).toBeFalsy(); + }); + + test('You update meta as a string', () => { + expect( + updateRulesSchema.validate< + Partial & { meta: string }> + >({ + id: 'rule-1', + meta: 'should not work', + }).error + ).toBeTruthy(); + }); + + test('filters cannot be a string', () => { + expect( + updateRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + type: 'query', + filters: 'some string', + }).error + ).toBeTruthy(); + }); }); - describe('find signals schema', () => { + describe('find rules schema', () => { test('empty objects do validate', () => { - expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); test('all values validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ per_page: 5, page: 1, sort_field: 'some field', fields: ['field 1', 'field 2'], + filter: 'some filter', + sort_order: 'asc', }).error ).toBeFalsy(); }); test('made up parameters do not validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1460,31 +1840,31 @@ describe('schemas', () => { test('per_page validates', () => { expect( - findSignalsSchema.validate>({ per_page: 5 }).error + findRulesSchema.validate>({ per_page: 5 }).error ).toBeFalsy(); }); test('page validates', () => { expect( - findSignalsSchema.validate>({ page: 5 }).error + findRulesSchema.validate>({ page: 5 }).error ).toBeFalsy(); }); test('sort_field validates', () => { expect( - findSignalsSchema.validate>({ sort_field: 'some value' }).error + findRulesSchema.validate>({ sort_field: 'some value' }).error ).toBeFalsy(); }); test('fields validates with a string', () => { expect( - findSignalsSchema.validate>({ fields: ['some value'] }).error + findRulesSchema.validate>({ fields: ['some value'] }).error ).toBeFalsy(); }); test('fields validates with multiple strings', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ fields: ['some value 1', 'some value 2'], }).error ).toBeFalsy(); @@ -1492,44 +1872,95 @@ describe('schemas', () => { test('fields does not validate with a number', () => { expect( - findSignalsSchema.validate> & { fields: number[] }>({ + findRulesSchema.validate> & { fields: number[] }>({ fields: [5], }).error ).toBeTruthy(); }); test('per page has a default of 20', () => { - expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); }); test('page has a default of 1', () => { - expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + expect(findRulesSchema.validate>({}).value.page).toEqual(1); }); - }); - describe('querySignalSchema', () => { - test('empty objects do not validate', () => { + test('filter works with a string', () => { + expect( + findRulesSchema.validate>({ + filter: 'some value 1', + }).error + ).toBeFalsy(); + }); + + test('filter does not work with a number', () => { expect( - querySignalSchema.validate>({}).error + findRulesSchema.validate> & { filter: number }>({ + filter: 5, + }).error ).toBeTruthy(); }); + test('sort_order requires sort_field to work', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'asc', + }).error + ).toBeTruthy(); + }); + + test('sort_order and sort_field validate together', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'asc', + sort_field: 'some field', + }).error + ).toBeFalsy(); + }); + + test('sort_order validates with desc and sort_field', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'desc', + sort_field: 'some field', + }).error + ).toBeFalsy(); + }); + + test('sort_order does not validate with a string other than asc and desc', () => { + expect( + findRulesSchema.validate< + Partial> & { sort_order: string } + >({ + sort_order: 'some other string', + sort_field: 'some field', + }).error + ).toBeTruthy(); + }); + }); + + describe('queryRulesSchema', () => { + test('empty objects do not validate', () => { + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); + }); + test('both rule_id and id being supplied dot not validate', () => { expect( - querySignalSchema.validate>({ rule_id: '1', id: '1' }) + queryRulesSchema.validate>({ rule_id: '1', id: '1' }) .error ).toBeTruthy(); }); test('only id validates', () => { expect( - querySignalSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - querySignalSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 596850b4a11e4..664a98ad7d7dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; /* eslint-disable @typescript-eslint/camelcase */ const description = Joi.string(); @@ -22,12 +23,18 @@ const index = Joi.array() const interval = Joi.string(); const query = Joi.string(); const language = Joi.string().valid('kuery', 'lucene'); +const output_index = Joi.string(); const saved_id = Joi.string(); +const meta = Joi.object(); const max_signals = Joi.number().greater(0); const name = Joi.string(); +const risk_score = Joi.number() + .greater(-1) + .less(101); const severity = Joi.string(); const to = Joi.string(); const type = Joi.string().valid('filter', 'query', 'saved_query'); +const queryFilter = Joi.string(); const references = Joi.array() .items(Joi.string()) .single(); @@ -38,35 +45,66 @@ const page = Joi.number() .min(1) .default(1); const sort_field = Joi.string(); +const sort_order = Joi.string().valid('asc', 'desc'); const tags = Joi.array().items(Joi.string()); const fields = Joi.array() .items(Joi.string()) .single(); /* eslint-enable @typescript-eslint/camelcase */ -export const createSignalsSchema = Joi.object({ +export const createRulesSchema = Joi.object({ description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), filter: filter.when('type', { is: 'filter', then: Joi.required(), otherwise: Joi.forbidden() }), - filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), + filters: Joi.when('type', { + is: 'query', + then: filters.optional(), + otherwise: Joi.when('type', { + is: 'saved_query', + then: filters.optional(), + otherwise: Joi.forbidden(), + }), + }), from: from.required(), rule_id, immutable: immutable.default(false), - index: index.required(), + index, interval: interval.default('5m'), - query: query.when('type', { is: 'query', then: Joi.required(), otherwise: Joi.forbidden() }), - language: language.when('type', { + query: Joi.when('type', { is: 'query', - then: Joi.required(), - otherwise: Joi.forbidden(), + then: Joi.when('filters', { + is: Joi.exist(), + then: query + .optional() + .allow('') + .default(''), + otherwise: Joi.required(), + }), + otherwise: Joi.when('type', { + is: 'saved_query', + then: query.optional(), + otherwise: Joi.forbidden(), + }), + }), + language: Joi.when('type', { + is: 'query', + then: language.required(), + otherwise: Joi.when('type', { + is: 'saved_query', + then: language.optional(), + otherwise: Joi.forbidden(), + }), }), + output_index, saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), otherwise: Joi.forbidden(), }), - max_signals: max_signals.default(100), + meta, + risk_score: risk_score.required(), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), severity: severity.required(), tags: tags.default([]), @@ -75,29 +113,52 @@ export const createSignalsSchema = Joi.object({ references: references.default([]), }); -export const updateSignalSchema = Joi.object({ +export const updateRulesSchema = Joi.object({ description, enabled, false_positives, filter: filter.when('type', { is: 'filter', then: Joi.optional(), otherwise: Joi.forbidden() }), - filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), + filters: Joi.when('type', { + is: 'query', + then: filters.optional(), + otherwise: Joi.when('type', { + is: 'saved_query', + then: filters.optional(), + otherwise: Joi.forbidden(), + }), + }), from, rule_id, id, immutable, index, interval, - query: query.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), - language: language.when('type', { + query: Joi.when('type', { is: 'query', - then: Joi.optional(), - otherwise: Joi.forbidden(), + then: query.optional(), + otherwise: Joi.when('type', { + is: 'saved_query', + then: query.optional(), + otherwise: Joi.forbidden(), + }), + }), + language: Joi.when('type', { + is: 'query', + then: language.optional(), + otherwise: Joi.when('type', { + is: 'saved_query', + then: language.optional(), + otherwise: Joi.forbidden(), + }), }), + output_index, saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.optional(), otherwise: Joi.forbidden(), }), + meta, + risk_score, max_signals, name, severity, @@ -107,14 +168,20 @@ export const updateSignalSchema = Joi.object({ references, }).xor('id', 'rule_id'); -export const querySignalSchema = Joi.object({ +export const queryRulesSchema = Joi.object({ rule_id, id, }).xor('id', 'rule_id'); -export const findSignalsSchema = Joi.object({ +export const findRulesSchema = Joi.object({ + fields, + filter: queryFilter, per_page, page, - sort_field, - fields, + sort_field: Joi.when(Joi.ref('sort_order'), { + is: Joi.exist(), + then: sort_field.required(), + otherwise: sort_field.optional(), + }), + sort_order, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts new file mode 100644 index 0000000000000..d03d68417dd5d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from './__mocks__/_mock_server'; + +import { updateRulesRoute } from './update_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + updateActionResult, + getUpdateRequest, + typicalPayload, + getFindResultWithSingleHit, + typicalFilterPayload, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; + +describe('update_rules', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient } = createMockServer()); + updateRulesRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getUpdateRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getUpdateRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + updateRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + updateRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + updateRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: { + payload: noId, + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 404 if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if type is filter', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalFilterPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'something-made-up', + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts new file mode 100644 index 0000000000000..1cc65054527c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { updateRules } from '../alerts/update_rules'; +import { UpdateRulesRequest } from '../alerts/types'; +import { updateRulesSchema } from './schemas'; +import { ServerFacade } from '../../../types'; +import { getIdError, transformOrError } from './utils'; + +export const createUpdateRulesRoute: Hapi.ServerRoute = { + method: 'PUT', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:signals-all'], + validate: { + options: { + abortEarly: false, + }, + payload: updateRulesSchema, + }, + }, + async handler(request: UpdateRulesRequest, headers) { + const { + description, + enabled, + false_positives: falsePositives, + filter, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + references, + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + const rule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + references, + }); + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + }, +}; + +export const updateRulesRoute = (server: ServerFacade) => { + server.route(createUpdateRulesRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts deleted file mode 100644 index 7288d18628316..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ /dev/null @@ -1,154 +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 { - createMockServer, - createMockServerWithoutActionClientDecoration, - createMockServerWithoutAlertClientDecoration, - createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; - -import { updateSignalsRoute } from './update_signals_route'; -import { ServerInjectOptions } from 'hapi'; -import { - getFindResult, - getResult, - updateActionResult, - getUpdateRequest, - typicalPayload, - getFindResultWithSingleHit, - typicalFilterPayload, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; - -describe('update_signals', () => { - let { server, alertsClient, actionsClient } = createMockServer(); - - beforeEach(() => { - jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - updateSignalsRoute(server); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getUpdateRequest()); - expect(statusCode).toBe(200); - }); - - test('returns 404 when updating a single signal that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getUpdateRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if actionClient is not available on the route', async () => { - const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateSignalsRoute(serverWithoutActionClient); - const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateSignalsRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); - expect(statusCode).toBe(404); - }); - - test('returns 404 if alertClient and actionClient are both not available on the route', async () => { - const { - serverWithoutActionOrAlertClient, - } = createMockServerWithoutActionOrAlertClientDecoration(); - updateSignalsRoute(serverWithoutActionOrAlertClient); - const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); - expect(statusCode).toBe(404); - }); - }); - - describe('validation', () => { - test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - const { rule_id, ...noId } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'PUT', - url: DETECTION_ENGINE_RULES_URL, - payload: { - payload: noId, - }, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - - test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const request: ServerInjectOptions = { - method: 'PUT', - url: DETECTION_ENGINE_RULES_URL, - payload: typicalPayload(), - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(404); - }); - - test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const request: ServerInjectOptions = { - method: 'PUT', - url: DETECTION_ENGINE_RULES_URL, - payload: typicalPayload(), - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - - test('returns 200 if type is filter', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const request: ServerInjectOptions = { - method: 'PUT', - url: DETECTION_ENGINE_RULES_URL, - payload: typicalFilterPayload(), - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); - - test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - const { type, ...noType } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'PUT', - url: DETECTION_ENGINE_RULES_URL, - payload: { - ...noType, - type: 'something-made-up', - }, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts deleted file mode 100644 index fe507b348d349..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateSignal } from '../alerts/update_signals'; -import { UpdateSignalsRequest } from '../alerts/types'; -import { updateSignalSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { getIdError, transformOrError } from './utils'; - -export const createUpdateSignalsRoute: Hapi.ServerRoute = { - method: 'PUT', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:signals-all'], - validate: { - options: { - abortEarly: false, - }, - payload: updateSignalSchema, - }, - }, - async handler(request: UpdateSignalsRequest, headers) { - const { - description, - enabled, - false_positives: falsePositives, - filter, - from, - immutable, - query, - language, - // eslint-disable-next-line @typescript-eslint/camelcase - saved_id: savedId, - filters, - // eslint-disable-next-line @typescript-eslint/camelcase - rule_id: ruleId, - id, - index, - interval, - // eslint-disable-next-line @typescript-eslint/camelcase - max_signals: maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - } = request.payload; - - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - - const signal = await updateSignal({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - savedId, - filters, - id, - ruleId, - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - }); - if (signal != null) { - return transformOrError(signal); - } else { - return getIdError({ id, ruleId }); - } - }, -}; - -export const updateSignalsRoute = (server: ServerFacade) => { - server.route(createUpdateSignalsRoute); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 69f25e84d995c..632778d78dab7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { - transformAlertToSignal, + transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, @@ -14,27 +14,29 @@ import { import { getResult } from './__mocks__/request_responses'; describe('utils', () => { - describe('transformAlertToSignal', () => { + describe('transformAlertToRule', () => { test('should work with a full data set', () => { - const fullSignal = getResult(); - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, false_positives: [], from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', + risk_score: 50, rule_id: 'rule-1', language: 'kuery', max_signals: 100, name: 'Detect Root/Admin Users', + output_index: '.siem-signals', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -43,23 +45,25 @@ describe('utils', () => { }); test('should work with a partial data set missing data', () => { - const fullSignal = getResult(); - const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, false_positives: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', interval: '5m', + risk_score: 50, rule_id: 'rule-1', max_signals: 100, name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -68,25 +72,27 @@ describe('utils', () => { }); test('should omit query if query is null', () => { - const fullSignal = getResult(); - fullSignal.alertTypeParams.query = null; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, false_positives: [], from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', interval: '5m', + risk_score: 50, rule_id: 'rule-1', language: 'kuery', max_signals: 100, name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -95,25 +101,27 @@ describe('utils', () => { }); test('should omit query if query is undefined', () => { - const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, false_positives: [], from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', interval: '5m', rule_id: 'rule-1', + risk_score: 50, language: 'kuery', max_signals: 100, name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -122,23 +130,85 @@ describe('utils', () => { }); test('should omit a mix of undefined, null, and missing fields', () => { - const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; - fullSignal.alertTypeParams.language = null; - const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', false_positives: [], id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', rule_id: 'rule-1', + risk_score: 50, max_signals: 100, name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should return enabled is equal to false', () => { + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + from: 'now-6m', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + risk_score: 50, + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should return immutable is equal to false', () => { + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + from: 'now-6m', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + risk_score: 50, + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', updated_by: 'elastic', tags: [], to: 'now', @@ -208,8 +278,11 @@ describe('utils', () => { false_positives: [], from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', + risk_score: 50, rule_id: 'rule-1', language: 'kuery', max_signals: 100, @@ -217,7 +290,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -243,16 +315,18 @@ describe('utils', () => { false_positives: [], from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', rule_id: 'rule-1', + risk_score: 50, language: 'kuery', max_signals: 100, name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 4d653210b2bff..eb0ae49436bca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -5,8 +5,8 @@ */ import Boom from 'boom'; -import { pickBy, identity } from 'lodash/fp'; -import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; +import { pickBy } from 'lodash/fp'; +import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; export const getIdError = ({ id, @@ -26,47 +26,49 @@ export const getIdError = ({ // Transforms the data but will remove any null or undefined it encounters and not include // those on the export -export const transformAlertToSignal = (signal: SignalAlertType): Partial => { - return pickBy(identity, { - created_by: signal.createdBy, - description: signal.alertTypeParams.description, - enabled: signal.enabled, - false_positives: signal.alertTypeParams.falsePositives, - filter: signal.alertTypeParams.filter, - filters: signal.alertTypeParams.filters, - from: signal.alertTypeParams.from, - id: signal.id, - immutable: signal.alertTypeParams.immutable, - index: signal.alertTypeParams.index, - interval: signal.interval, - rule_id: signal.alertTypeParams.ruleId, - language: signal.alertTypeParams.language, - max_signals: signal.alertTypeParams.maxSignals, - name: signal.name, - query: signal.alertTypeParams.query, - references: signal.alertTypeParams.references, - saved_id: signal.alertTypeParams.savedId, - severity: signal.alertTypeParams.severity, - size: signal.alertTypeParams.size, - updated_by: signal.updatedBy, - tags: signal.alertTypeParams.tags, - to: signal.alertTypeParams.to, - type: signal.alertTypeParams.type, +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filter: alert.params.filter, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.params.tags, + to: alert.params.to, + type: alert.params.type, }); }; export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; } else { return new Boom('Internal error transforming', { statusCode: 500 }); } }; -export const transformOrError = (signal: unknown): Partial | Boom => { - if (isAlertType(signal)) { - return transformAlertToSignal(signal); +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); } else { return new Boom('Internal error transforming', { statusCode: 500 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md index b3ab0011e1f8f..8d617a8de3fcd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md @@ -4,6 +4,7 @@ search which is not available in the DEV console for the detection engine. Before beginning ensure in your .zshrc/.bashrc you have your user, password, and url set: Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -21,6 +22,7 @@ And that you have the latest version of [NodeJS](https://nodejs.org/en/), [CURL](https://curl.haxx.se), and [jq](https://stedolan.github.io/jq/) installed. If you have homebrew you can install using brew like so + ``` brew install jq ``` @@ -29,10 +31,9 @@ After that you can execute scripts within this folder by first ensuring your current working directory is `./scripts` and then running any scripts within that folder. -Example to add a signal to the system +Example to add a rule to the system ``` cd ./scripts -./post_signal.sh ./signals/root_or_admin_1.json +./post_rule.sh ./rules/root_or_admin_1.json ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh new file mode 100755 index 0000000000000..e4d345eec0b65 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +node ../../../../scripts/convert_saved_search_to_rules.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh deleted file mode 100755 index 802273c67849d..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -node ../../../../scripts/convert_saved_search_to_signals.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh new file mode 100755 index 0000000000000..2db5740c79bb8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_rule_by_id.sh ${id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh new file mode 100755 index 0000000000000..80ef849828b78 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_rule_by_rule_id.sh ${rule_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh deleted file mode 100755 index 73882c78edfb8..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Example: ./delete_signal_by_id.sh ${id} -curl -s -k \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh deleted file mode 100755 index 2b51146e6e1a0..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Example: ./delete_signal_by_rule_id.sh ${rule_id} -curl -s -k \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh new file mode 100755 index 0000000000000..34b6208947c57 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +FILTER=${1:-'alert.attributes.enabled:%20true'} + +# Example: ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" +# Example: ./find_rule_by_filter.sh "alert.attributes.name:%20Detect*" +# The %20 is just an encoded space that is typical of URL's. +# Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?filter=$FILTER | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh new file mode 100755 index 0000000000000..520b4afa24cd2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./find_rules.sh +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh new file mode 100755 index 0000000000000..8e6690d848db4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +SORT=${1:-'enabled'} +ORDER=${2:-'asc'} + +# Example: ./find_rules_sort.sh enabled asc +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh index 2b26c939a924c..fbcd159cd24e8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh @@ -18,5 +18,5 @@ TYPE=${1:-alert} # https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html#saved-objects-api-find-request curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/saved_objects/_find?type=$TYPE \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/saved_objects/_find?type=$TYPE \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh deleted file mode 100755 index 473c786936190..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Example: ./find_signals.sh -curl -s -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index e2177bb750057..7804439ce0734 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/action/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_find \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh index 7937f2f99a37f..8d8cbdd70a803 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/action/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/types \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh index 3abc8c9adee62..f42d4a52594a7 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md#get-apialert_find-find-alerts curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/alert/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/_find \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh index 7f7361a6252bc..a7c6fa567ecdd 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md#get-apialerttypes-list-alert-types curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/alert/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/types \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh new file mode 100755 index 0000000000000..dba5652390ea9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_rule_by_id.sh {rule_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh new file mode 100755 index 0000000000000..114b6570a03e2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_rule_by_rule_id.sh {rule_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh index 4829beba86743..5b5344bc205ff 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh @@ -14,5 +14,5 @@ set -e # https://www.elastic.co/guide/en/kibana/master/saved-objects-api-get.html curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/saved_objects/$1/$2 \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/saved_objects/$1/$2 \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh deleted file mode 100755 index d10f347ff1f9e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Example: ./get_signal_by_id.sh {rule_id} -curl -s -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh deleted file mode 100755 index 302936fcb523e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Example: ./get_signal_by_rule_id.sh {rule_id} -curl -s -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh new file mode 100755 index 0000000000000..591cf7625e2e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=(${@:-./rules/root_or_admin_1.json}) + +# Example: ./post_rule.sh +# Example: ./post_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" +do { + [ -e "$RULE" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ + -d @${RULE} \ + | jq .; +} & +done + +wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh deleted file mode 100755 index 837454dea71e6..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_1.json}) - -# Example: ./post_signal.sh -# Example: ./post_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" -do { - [ -e "$SIGNAL" ] || continue - curl -s -k \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ - | jq .; -} & -done - -wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh new file mode 100755 index 0000000000000..53e7bb504746d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default of 100 if no argument is specified +NUMBER=${1:-100} + +# Example: ./post_x_rules.sh +# Example: ./post_x_rules.sh 200 +for i in $(seq 1 $NUMBER); +do { + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ + --data "{ + \"rule_id\": \"${i}\", + \"risk_score\": \"50\", + \"description\": \"Detecting root and admin users\", + \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], + \"interval\": \"24h\", + \"name\": \"Detect Root/Admin Users\", + \"severity\": \"high\", + \"type\": \"query\", + \"from\": \"now-6m\", + \"to\": \"now\", + \"query\": \"user.name: root or user.name: admin\", + \"language\": \"kuery\" + }" \ + | jq .; +} & +done + +wait \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh deleted file mode 100755 index 326d47280c306..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Uses a default of 100 if no argument is specified -NUMBER=${1:-100} - -# Example: ./post_x_signals.sh -# Example: ./post_x_signals.sh 200 -for i in $(seq 1 $NUMBER); -do { - curl -s -k \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/detection_engine/rules \ - --data "{ - \"rule_id\": \"${i}\", - \"description\": \"Detecting root and admin users\", - \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], - \"interval\": \"24h\", - \"name\": \"Detect Root/Admin Users\", - \"severity\": \"high\", - \"type\": \"query\", - \"from\": \"now-6m\", - \"to\": \"now\", - \"query\": \"user.name: root or user.name: admin\", - \"language\": \"kuery\" - }" \ - | jq .; -} & -done - -wait \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json new file mode 100644 index 0000000000000..c136c9b0fe808 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json @@ -0,0 +1,53 @@ +{ + "rule_id": "filters-with-empty-query", + "risk_score": 7, + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "output_index": ".siem-signals", + "language": "lucene", + "query": "", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json new file mode 100644 index 0000000000000..5b69fced90daf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json @@ -0,0 +1,52 @@ +{ + "rule_id": "filters-without-query", + "risk_score": 7, + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "output_index": ".siem-signals", + "language": "lucene", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json index c6b2999b0e0c0..b00a5929d9ef1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json @@ -1,7 +1,7 @@ { "rule_id": "rule-1", + "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json index 001b39bda5cbe..657439104e306 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json @@ -1,6 +1,5 @@ { "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json index 0b16d1ab03bd6..137cf7eedbccf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json @@ -1,7 +1,7 @@ { "rule_id": "rule-2", + "risk_score": 2, "description": "Detecting root and admin users over a long period of time", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Root/Admin Users over a long period of time", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json index f2d599b260f7b..b9160c95621ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json @@ -1,7 +1,7 @@ { "rule_id": "rule-3", + "risk_score": 3, "description": "Detecting root and admin users as an empty set", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json index 392877ec77df0..364e7f00c9571 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json @@ -1,7 +1,7 @@ { "rule_id": "rule-4", + "risk_score": 4, "description": "Detecting root and admin users with lucene", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json similarity index 87% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json index f99f718f2adee..eb7f2ae03b64b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json @@ -1,7 +1,7 @@ { "rule_id": "rule-5", + "risk_score": 5, "description": "Detecting root and admin users over 24 hours on windows", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json index c2d2eb1128deb..94f30bc9f92df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json @@ -1,7 +1,7 @@ { "rule_id": "rule-6", + "risk_score": 6, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json index 99bb9b69c462a..81ec19a4fd0ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json @@ -1,7 +1,7 @@ { "rule_id": "rule-7", + "risk_score": 7, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json index 07ffb26ea0eca..de24263c6af5c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json @@ -1,7 +1,7 @@ { "rule_id": "rule-8", + "risk_score": 8, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json similarity index 86% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json index 06e66bbac4b60..9bf2b1abf5f90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json @@ -1,7 +1,7 @@ { "rule_id": "rule-9", + "risk_score": 9, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json index cc4c3d2f7407f..2381e9e259c07 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json @@ -1,7 +1,7 @@ { "rule_id": "rule-9999", + "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json index 0dfb92b9098cb..ee8fe1fc93fb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json @@ -1,7 +1,7 @@ { "rule_id": "rule-9999", + "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json new file mode 100644 index 0000000000000..ed8f2e5745bea --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json @@ -0,0 +1,19 @@ +{ + "rule_id": "rule-meta-data", + "risk_score": 1, + "description": "Detecting root and admin users", + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "references": ["http://www.example.com", "https://ww.example.com"], + "meta": { + "anything_i_want": { + "total_meta_for_ui_needs": true + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json new file mode 100644 index 0000000000000..721644acd989d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json @@ -0,0 +1,12 @@ +{ + "rule_id": "saved-query-1", + "risk_score": 5, + "description": "Detecting root and admin users", + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "saved_query", + "from": "now-6m", + "to": "now", + "saved_id": "test-saveid" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json new file mode 100644 index 0000000000000..b733b6bb8c592 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json @@ -0,0 +1,14 @@ +{ + "rule_id": "saved-query-2", + "risk_score": 5, + "description": "Detecting root and admin users", + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "saved_query", + "from": "now-6m", + "to": "now", + "saved_id": "test-saveid-2", + "query": "user.name: root or user.name: admin", + "language": "kuery" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json new file mode 100644 index 0000000000000..df1b37f19bf29 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json @@ -0,0 +1,12 @@ +{ + "rule_id": "saved-query-3", + "risk_score": 5, + "description": "Detecting root and admin users", + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "saved_query", + "from": "now-6m", + "to": "now", + "saved_id": "test-saveid-3" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json index 5592ef7bdfd0c..09ddfb1c34a92 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json @@ -1,7 +1,7 @@ { "rule_id": "rule-1", + "risk_score": 98, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json index a15f671d6a0b1..8a3c765519ef3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json @@ -1,7 +1,7 @@ { "rule_id": "rule-1", + "risk_score": 78, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json similarity index 79% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json index d18ed01bd13d6..a43398bd6876a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json @@ -1,7 +1,7 @@ { "rule_id": "rule-longmont", + "risk_score": 5, "description": "Detect Longmont activity", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Longmont activity", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json deleted file mode 100644 index 17dc207a62fa6..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "rule_id": "saved-query-1", - "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], - "interval": "5m", - "name": "Detect Root/Admin Users", - "severity": "high", - "type": "saved_query", - "from": "now-6m", - "to": "now", - "saved_id": "Test Query From SIEM" -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh new file mode 100755 index 0000000000000..8e1abc7045602 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=(${@:-./rules/root_or_admin_update_1.json}) + +# Example: ./update_rule.sh +# Example: ./update_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" +do { + [ -e "$RULE" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ + -d @${RULE} \ + | jq .; +} & +done + +wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh deleted file mode 100755 index 1d16aa6fc7062..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License; -# you may not use this file except in compliance with the Elastic License. -# - -set -e -./check_env_variables.sh - -# Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) - -# Example: ./update_signal.sh -# Example: ./update_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" -do { - [ -e "$SIGNAL" ] || continue - curl -s -k \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X PUT ${KIBANA_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ - | jq .; -} & -done - -wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json index dd80e786a3121..dfe3caed5b71a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json @@ -23,38 +23,162 @@ } } }, - "id": { - "type": "keyword" + "rule": { + "properties": { + "id": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "output_index": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "filter": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } }, "original_time": { "type": "date" }, - "rule_revision": { - "type": "long" - }, - "rule_id": { - "type": "keyword" - }, - "rule_type": { - "type": "keyword" - }, - "rule_query": { - "type": "keyword" - }, - "index_patterns": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "description": { - "type": "text" + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } }, - "severity": { + "status": { "type": "keyword" - }, - "references": { - "type": "text" } } }, diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts index e655485638e16..98bd6944c1b51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts @@ -27,8 +27,7 @@ export const buildEventsOverTimeQuery = ({ ]; const getHistogramAggregation = () => { - const minIntervalSeconds = 10; - const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds); + const interval = calculateTimeseriesInterval(from, to); const histogramTimestampField = '@timestamp'; const dateHistogram = { date_histogram: { diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 39babc58ee138..07b748024743c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -18,6 +18,7 @@ import { NetworkHttpData, NetworkHttpEdges, NetworkTopNFlowEdges, + MatrixOverOrdinalHistogramData, } from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; @@ -140,6 +141,7 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { ); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; const edges = networkDnsEdges.splice(cursorStart, querySize - cursorStart); + const histogram = getHistogramData(edges); const inspect = { dsl: [inspectStringifyObject(dsl)], response: [inspectStringifyObject(response)], @@ -154,6 +156,7 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { showMorePagesIndicator, }, totalCount, + histogram, }; } @@ -194,6 +197,32 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { } } +const getHistogramData = ( + data: NetworkDnsEdges[] +): MatrixOverOrdinalHistogramData[] | undefined => { + if (!Array.isArray(data)) return undefined; + return data.reduce( + (acc: MatrixOverOrdinalHistogramData[], { node: { dnsBytesOut, dnsBytesIn, _id } }) => { + if (_id != null && dnsBytesOut != null && dnsBytesIn != null) + return [ + ...acc, + { + x: _id, + y: dnsBytesOut, + g: 'DNS Bytes Out', + }, + { + x: _id, + y: dnsBytesIn, + g: 'DNS Bytes In', + }, + ]; + return acc; + }, + [] + ); +}; + const getTopNFlowEdges = ( response: DatabaseSearchResponse, options: NetworkTopNFlowRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 2769199ad1fb5..c53805dc95fe7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; import { ConfigurationAdapter } from './configuration'; import { Events } from './events'; @@ -22,10 +23,12 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; +import { SearchTypes, OutputRuleAlertRest } from './detection_engine/alerts/types'; export * from './hosts'; export interface AppDomainLibs { + anomalies: Anomalies; authentications: Authentications; events: Events; fields: IndexFields; @@ -62,26 +65,23 @@ export interface SiemContext { req: FrameworkRequest; } -export interface SignalHit { - signal: { - '@timestamp': string; +export interface Signal { + rule: Partial; + parent: { id: string; - rule_revision: number; - rule_id: string | undefined | null; - rule_type: string; - parent: { - id: string; - type: string; - index: string; - depth: number; - }; - name: string; - severity: string; - description: string; - original_time: string; - index_patterns: string[]; - references: string[]; + type: string; + index: string; + depth: number; }; + original_time: string; + original_event?: SearchTypes; + status: 'open' | 'closed'; +} + +export interface SignalHit { + '@timestamp': string; + event: object; + signal: Partial; } export interface TotalValue { diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts index 3eaaa6c30a4fa..752c686b243ac 100644 --- a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts @@ -91,8 +91,7 @@ export const calculateAuto = { export const calculateTimeseriesInterval = ( lowerBoundInMsSinceEpoch: number, - upperBoundInMsSinceEpoch: number, - minIntervalSeconds: number + upperBoundInMsSinceEpoch: number ) => { const duration = moment.duration(upperBoundInMsSinceEpoch - lowerBoundInMsSinceEpoch, 'ms'); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index 2d85a61b04852..bc48d6d6312fb 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -18,6 +18,8 @@ jest.mock('ui/i18n', () => { return { I18nContext }; }); +jest.mock('ui/new_platform'); + const POLICY_NAME = 'my_policy'; const SNAPSHOT_NAME = 'my_snapshot'; const MIN_COUNT = '5'; @@ -141,6 +143,25 @@ describe.skip('', () => { 'Minimum count cannot be greater than maximum count.', ]); }); + + test('should not allow negative values for the delete after, minimum and maximum counts', () => { + const { find, form } = testBed; + + form.setInputValue('expireAfterValueInput', '-1'); + find('expireAfterValueInput').simulate('blur'); + + form.setInputValue('minCountInput', '-1'); + find('minCountInput').simulate('blur'); + + form.setInputValue('maxCountInput', '-1'); + find('maxCountInput').simulate('blur'); + + expect(form.getErrorsMessages()).toEqual([ + 'Delete after cannot be negative.', + 'Minimum count cannot be negative.', + 'Maximum count cannot be negative.', + ]); + }); }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/data_placeholder.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/data_placeholder.tsx index 5908a34a67d73..92e82e6800226 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/data_placeholder.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/data_placeholder.tsx @@ -13,7 +13,7 @@ interface Props { children: React.ReactNode; } -export const DataPlaceholder: React.SFC = ({ data, children }) => { +export const DataPlaceholder: React.FC = ({ data, children }) => { const { core: { i18n }, } = useAppDependencies(); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx index c88cbd2736df6..df7e2c8807d9f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx @@ -85,7 +85,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ } describedByIds={['expirationDescription']} isInvalid={touched.expireAfterValue && Boolean(errors.expireAfterValue)} - error={errors.expireAfter} + error={errors.expireAfterValue} fullWidth > @@ -100,6 +100,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="expireAfterValueInput" + min={0} /> @@ -167,6 +168,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="minCountInput" + min={0} /> @@ -179,6 +181,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ /> } describedByIds={['countDescription']} + isInvalid={touched.maxCount && Boolean(errors.maxCount)} error={errors.maxCount} fullWidth > @@ -193,6 +196,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="maxCountInput" + min={0} /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_type_logo.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_type_logo.tsx index 9ce6eabf66b7c..881d03deaf4f9 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_type_logo.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_type_logo.tsx @@ -14,7 +14,7 @@ interface Props { [key: string]: any; } -export const RepositoryTypeLogo: React.SFC = ({ type, ...rest }) => { +export const RepositoryTypeLogo: React.FC = ({ type, ...rest }) => { const typeLogoMap: { [key: string]: any } = { [REPOSITORY_TYPES.fs]: 'storage', [REPOSITORY_TYPES.url]: 'eye', diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx index 07fe0a58138f3..b14e5c1a81424 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx @@ -15,7 +15,7 @@ interface Props { state: any; } -export const SnapshotState: React.SFC = ({ state }) => { +export const SnapshotState: React.FC = ({ state }) => { const { core: { i18n }, } = useAppDependencies(); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx index ea2b8b9904d8f..eab31bae7df24 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx @@ -16,7 +16,7 @@ interface Props { snapshotState: string; } -export const TabFailures: React.SFC = ({ indexFailures, snapshotState }) => { +export const TabFailures: React.FC = ({ indexFailures, snapshotState }) => { const { core: { i18n: { FormattedMessage }, diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index bbec23d30622d..d3d32cb149064 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -30,7 +30,7 @@ interface Props { snapshotDetails: SnapshotDetails; } -export const TabSummary: React.SFC = ({ snapshotDetails }) => { +export const TabSummary: React.FC = ({ snapshotDetails }) => { const { core: { i18n: { FormattedMessage }, diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts index 80734d2f0522c..3f27da82bf56d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts @@ -28,7 +28,9 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { schedule: [], repository: [], indices: [], + expireAfterValue: [], minCount: [], + maxCount: [], }, }; @@ -92,6 +94,34 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { }) ); } + + if (retention && retention.expireAfterValue && retention.expireAfterValue < 0) { + validation.errors.expireAfterValue.push( + i18n.translate( + 'xpack.snapshotRestore.policyValidation.invalidNegativeDeleteAfterErrorMessage', + { + defaultMessage: 'Delete after cannot be negative.', + } + ) + ); + } + + if (retention && retention.minCount && retention.minCount < 0) { + validation.errors.minCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMinCountErrorMessage', { + defaultMessage: 'Minimum count cannot be negative.', + }) + ); + } + + if (retention && retention.maxCount && retention.maxCount < 0) { + validation.errors.maxCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMaxCountErrorMessage', { + defaultMessage: 'Maximum count cannot be negative.', + }) + ); + } + // Remove fields with no errors validation.errors = Object.entries(validation.errors) .filter(([key, value]) => value.length > 0) diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 598d115a39e49..8f995d3c12c2a 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -126,7 +126,6 @@ export const spaces = (kibana: Record) => kibanaIndex: config.get('kibana.index'), }, savedObjects: server.savedObjects, - usage: server.usage, tutorial: { addScopedTutorialContextFactory: server.addScopedTutorialContextFactory, }, diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx index 0211fe7e82643..0d9751ca43db9 100644 --- a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx @@ -5,7 +5,7 @@ */ import { EuiAvatar, isValidHex } from '@elastic/eui'; -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { MAX_SPACE_INITIALS } from '../../common'; import { Space } from '../../common/model/space'; import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from '../lib/space_attributes'; @@ -17,7 +17,7 @@ interface Props { announceSpaceName?: boolean; } -export const SpaceAvatar: SFC = (props: Props) => { +export const SpaceAvatar: FC = (props: Props) => { const { space, size, announceSpaceName, ...rest } = props; const spaceName = space.name ? space.name.trim() : ''; diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx b/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx index 7f04189b50d5e..cb70daead1bd2 100644 --- a/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx @@ -30,7 +30,7 @@ test('it is clickable', () => { const clickHandler = jest.fn(); const wrapper = mount(); - wrapper.simulate('click'); + wrapper.find('button').simulate('click'); expect(clickHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx index d75b8abbe592e..53c8be7163341 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx @@ -14,7 +14,7 @@ interface Props { intl: InjectedIntl; } -const ConfirmAlterActiveSpaceModalUI: React.SFC = props => ( +const ConfirmAlterActiveSpaceModalUI: React.FC = props => ( void; } -export const SpacesDescription: SFC = (props: Props) => { +export const SpacesDescription: FC = (props: Props) => { const panelProps = { className: 'spcDescription', title: 'Spaces', diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx index 2fa0f8ca605b0..d5c693df58b28 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx @@ -11,7 +11,7 @@ import { import React from 'react'; import { ButtonProps } from '../types'; -export const SpacesHeaderNavButton: React.SFC = props => ( +export const SpacesHeaderNavButton: React.FC = props => ( { - (async () => { - // The code block below can't await directly within "afterPluginsInit" - // callback due to circular dependency. The server isn't "ready" until - // this code block finishes. Migrations wait for server to be ready before - // executing. Saved objects repository waits for migrations to finish before - // finishing the request. To avoid this, we'll await within a separate - // function block. - await this.kbnServer.server.kibanaMigrator.runMigrations(); - plugin.start(); - })(); + plugin.start(); }); server.expose(setupContract); }, diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts index adc4bfd1b5918..af55732691bb0 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts index 85daad6c7fd52..e6792958ab5d2 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 16100089a9e56..b465392a50ae1 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -104,7 +104,7 @@ export function createSearchItems( const filters = fs.length ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(config); - combinedQuery = esQuery.buildEsQuery(indexPattern || null, [query], filters, esQueryConfigs); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } return { diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts index 08cf7d0046e97..82d5362e21c02 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts @@ -5,10 +5,12 @@ */ export { - isKibanaContextInitialized, + useKibanaContext, + InitializedKibanaContextValue, KibanaContext, KibanaContextValue, SavedSearchQuery, + RenderOnlyWithInitializedKibanaContext, } from './kibana_context'; export { KibanaProvider } from './kibana_provider'; export { useCurrentIndexPattern } from './use_current_index_pattern'; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index 2e4f0dfd4696a..e3515991e7bb1 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createContext } from 'react'; +import React, { createContext, useContext, FC } from 'react'; import { IndexPattern as IndexPatternType, @@ -15,14 +15,15 @@ import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kib import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server'; // set() method is missing in original d.ts -export interface KibanaConfigTypeFix extends KibanaConfig { +interface KibanaConfigTypeFix extends KibanaConfig { set(key: string, value: any): void; } interface UninitializedKibanaContextValue { initialized: boolean; } -interface InitializedKibanaContextValue { + +export interface InitializedKibanaContextValue { combinedQuery: any; currentIndexPattern: IndexPatternType; currentSavedSearch: SavedSearch; @@ -41,3 +42,37 @@ export function isKibanaContextInitialized(arg: any): arg is InitializedKibanaCo export type SavedSearchQuery = object; export const KibanaContext = createContext({ initialized: false }); + +/** + * Custom hook to get the current kibanaContext. + * + * @remarks + * This hook should only be used in components wrapped in `RenderOnlyWithInitializedKibanaContext`, + * otherwise it will throw an error when KibanaContext hasn't been initialized yet. + * In return you get the benefit of not having to check if it's been initialized in the component + * where it's used. + * + * @returns `kibanaContext` + */ +export const useKibanaContext = () => { + const kibanaContext = useContext(KibanaContext); + + if (!isKibanaContextInitialized(kibanaContext)) { + throw new Error('useKibanaContext: kibanaContext not initialized'); + } + + return kibanaContext; +}; + +/** + * Wrapper component to render children only if `kibanaContext` has been initialized. + * In combination with `useKibanaContext` this avoids having to check for the initialization + * in consuming components. + * + * @returns `children` or `null` depending on whether `kibanaContext` is initialized or not. + */ +export const RenderOnlyWithInitializedKibanaContext: FC = ({ children }) => { + const kibanaContext = useContext(KibanaContext); + + return isKibanaContextInitialized(kibanaContext) ? <>{children} : null; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts index e4b0725c324b4..12c5bde171b8b 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts @@ -12,7 +12,7 @@ export const useCurrentIndexPattern = () => { const context = useContext(KibanaContext); if (!isKibanaContextInitialized(context)) { - throw new Error('currentIndexPattern is undefined'); + throw new Error('useCurrentIndexPattern: kibanaContext not initialized'); } return context.currentIndexPattern; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx index 7617237e72a7d..9ff235fb40d8a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx @@ -15,7 +15,7 @@ interface Props { testSubj?: string; } -export const DropDown: React.SFC = ({ +export const DropDown: React.FC = ({ changeHandler, options, placeholder = 'Search ...', diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx index 89f734f9bf268..cf3cd24c0439c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx @@ -22,7 +22,7 @@ interface Props { onChange(item: PivotAggsConfig): void; } -export const AggLabelForm: React.SFC = ({ +export const AggLabelForm: React.FC = ({ deleteHandler, item, otherAggNames, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx index b9437fee89efd..f3537dd7a523c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx @@ -24,12 +24,7 @@ export interface AggListProps { onChange(previousAggName: AggName, item: PivotAggsConfig): void; } -export const AggListForm: React.SFC = ({ - deleteHandler, - list, - onChange, - options, -}) => { +export const AggListForm: React.FC = ({ deleteHandler, list, onChange, options }) => { const listKeys = Object.keys(list); return ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx index 2b781e3a6c64d..7d07d79e7d283 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx @@ -14,7 +14,7 @@ export interface AggListSummaryProps { list: PivotAggsConfigDict; } -export const AggListSummary: React.SFC = ({ list }) => { +export const AggListSummary: React.FC = ({ list }) => { const aggNames = Object.keys(list); return ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 65dd8a34330a5..6c1e119ab38e0 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -40,12 +40,7 @@ interface Props { onChange(d: PivotAggsConfig): void; } -export const PopoverForm: React.SFC = ({ - defaultData, - otherAggNames, - onChange, - options, -}) => { +export const PopoverForm: React.FC = ({ defaultData, otherAggNames, onChange, options }) => { const isUnsupportedAgg = !isPivotAggsConfigWithUiSupport(defaultData); const [aggName, setAggName] = useState(defaultData.aggName); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx index d03c828e68fe2..c79da06ac8080 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx @@ -28,7 +28,7 @@ interface Props { onChange(item: PivotGroupByConfig): void; } -export const GroupByLabelForm: React.SFC = ({ +export const GroupByLabelForm: React.FC = ({ deleteHandler, item, otherAggNames, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx index 511eb091c6d42..1249ba50f2cc1 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx @@ -15,7 +15,7 @@ interface Props { optionsDataId: string; } -export const GroupByLabelSummary: React.SFC = ({ item, optionsDataId }) => { +export const GroupByLabelSummary: React.FC = ({ item, optionsDataId }) => { let interval: string | undefined; if (isGroupByDateHistogram(item)) { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx index 484314df5067d..c6f11a96cdfd4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx @@ -24,7 +24,7 @@ interface ListProps { onChange(id: string, item: PivotGroupByConfig): void; } -export const GroupByListForm: React.SFC = ({ +export const GroupByListForm: React.FC = ({ deleteHandler, list, onChange, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx index 46251e552e098..fc3c4b92c2a60 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx @@ -16,7 +16,7 @@ interface ListProps { list: PivotGroupByConfigDict; } -export const GroupByListSummary: React.SFC = ({ list }) => { +export const GroupByListSummary: React.FC = ({ list }) => { const listKeys = Object.keys(list); return ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx index e5ee72c0708ac..d05cf30f1d743 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx @@ -92,12 +92,7 @@ interface Props { onChange(item: PivotGroupByConfig): void; } -export const PopoverForm: React.SFC = ({ - defaultData, - otherAggNames, - onChange, - options, -}) => { +export const PopoverForm: React.FC = ({ defaultData, otherAggNames, onChange, options }) => { const isUnsupportedAgg = !isPivotGroupByConfigWithUiSupport(defaultData); const [agg, setAgg] = useState(defaultData.agg); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx index c69d051348654..9b83a3e5da8a8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx @@ -10,7 +10,7 @@ import { EuiBadge, EuiText } from '@elastic/eui'; import { EsDoc } from '../../../../common'; -export const ExpandedRow: React.SFC<{ item: EsDoc }> = ({ item }) => ( +export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( {Object.entries(item._source).map(([k, value]) => ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx index 8c870cc83636d..2b7d36cada3c6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx @@ -31,7 +31,7 @@ import { import { ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, SortingPropType, SORT_DIRECTION, } from '../../../../../shared_imports'; @@ -61,7 +61,7 @@ const CELL_CLICK_ENABLED = false; interface SourceIndexPreviewTitle { indexPatternTitle: string; } -const SourceIndexPreviewTitle: React.SFC = ({ indexPatternTitle }) => ( +const SourceIndexPreviewTitle: React.FC = ({ indexPatternTitle }) => ( {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { @@ -77,7 +77,7 @@ interface Props { cellClick?(search: string): void; } -export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, query }) => { +export const SourceIndexPreview: React.FC = React.memo(({ cellClick, query }) => { const [clearTable, setClearTable] = useState(false); const indexPattern = useCurrentIndexPattern(); @@ -183,8 +183,8 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que docFieldsCount = docFields.length; } - const columns: ColumnType[] = selectedFields.map(k => { - const column: ColumnType = { + const columns: Array> = selectedFields.map(k => { + const column: ColumnType = { field: `_source["${k}"]`, name: k, sortable: true, @@ -319,6 +319,8 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', }); + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( @@ -410,6 +412,9 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que itemId="_id" itemIdToExpandedRowMap={itemIdToExpandedRowMap} isExpandable={true} + rowProps={item => ({ + 'data-test-subj': `transformSourceIndexPreviewRow row-${item._id}`, + })} sorting={sorting} /> )} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx index 3bfe6c2cd0c5f..fb0a71baea321 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import ReactDOM from 'react-dom'; import { act } from 'react-dom/test-utils'; @@ -22,7 +22,7 @@ interface TestHookProps { callback: Callback; } -const TestHook: SFC = ({ callback }) => { +const TestHook: FC = ({ callback }) => { callback(); return null; }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 05b68631b7856..2ca3253d72b44 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; @@ -32,7 +32,7 @@ import { import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { ToastNotificationText } from '../../../../components'; import { useApi } from '../../../../hooks/use_api'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; @@ -60,7 +60,7 @@ interface Props { onChange(s: StepDetailsExposedState): void; } -export const StepCreateForm: SFC = React.memo( +export const StepCreateForm: FC = React.memo( ({ createIndexPattern, transformConfig, transformId, onChange, overrides }) => { const defaults = { ...getDefaultStepCreateState(), ...overrides }; @@ -73,7 +73,7 @@ export const StepCreateForm: SFC = React.memo( undefined ); - const kibanaContext = useContext(KibanaContext); + const kibanaContext = useKibanaContext(); useEffect(() => { onChange({ created, started, indexPatternId }); @@ -83,10 +83,6 @@ export const StepCreateForm: SFC = React.memo( const api = useApi(); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - async function createTransform() { setCreated(true); @@ -151,8 +147,8 @@ export const StepCreateForm: SFC = React.memo( } async function createAndStartTransform() { - const success = await createTransform(); - if (success) { + const acknowledged = await createTransform(); + if (acknowledged) { await startTransform(); } } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx index 1790b945df9cd..ebce5fe667f1e 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; -export const StepCreateSummary: SFC = React.memo(() => { +export const StepCreateSummary: FC = React.memo(() => { return null; }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx index 681a0f7e6ba3f..3f4c7e21d3947 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC, useEffect, useRef, useState } from 'react'; +import React, { FC, useEffect, useRef, useState } from 'react'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; @@ -21,7 +21,11 @@ import { EuiTitle, } from '@elastic/eui'; -import { ColumnType, MlInMemoryTableBasic, SORT_DIRECTION } from '../../../../../shared_imports'; +import { + ColumnType, + mlInMemoryTableBasicFactory, + SORT_DIRECTION, +} from '../../../../../shared_imports'; import { dictionaryToArray } from '../../../../../../common/types/common'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; @@ -38,7 +42,7 @@ import { } from '../../../../common'; import { getPivotPreviewDevConsoleStatement } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; +import { PreviewItem, PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; function sortColumns(groupByArr: PivotGroupByConfig[]) { return (a: string, b: string) => { @@ -68,7 +72,7 @@ interface PreviewTitleProps { previewRequest: PreviewRequestBody; } -const PreviewTitle: SFC = ({ previewRequest }) => { +const PreviewTitle: FC = ({ previewRequest }) => { const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', }); @@ -102,7 +106,7 @@ interface ErrorMessageProps { message: string; } -const ErrorMessage: SFC = ({ message }) => ( +const ErrorMessage: FC = ({ message }) => ( {message} @@ -114,7 +118,7 @@ interface PivotPreviewProps { query: PivotQuery; } -export const PivotPreview: SFC = React.memo(({ aggs, groupBy, query }) => { +export const PivotPreview: FC = React.memo(({ aggs, groupBy, query }) => { const [clearTable, setClearTable] = useState(false); const indexPattern = useCurrentIndexPattern(); @@ -210,7 +214,7 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, columnKeys.sort(sortColumns(groupByArr)); const columns = columnKeys.map(k => { - const column: ColumnType = { + const column: ColumnType = { field: k, name: k, sortable: true, @@ -256,6 +260,8 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, }, }; + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( @@ -273,6 +279,9 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, initialPageSize: 5, pageSizeOptions: [5, 10, 25], }} + rowProps={() => ({ + 'data-test-subj': 'transformPivotPreviewRow', + })} sorting={sorting} /> )} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 8eea3b020dcfe..b8f63ef697e78 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -37,9 +37,8 @@ import { KqlFilterBar } from '../../../../../shared_imports'; import { SwitchModal } from './switch_modal'; import { - isKibanaContextInitialized, - KibanaContext, - KibanaContextValue, + useKibanaContext, + InitializedKibanaContextValue, SavedSearchQuery, } from '../../../../lib/kibana'; @@ -75,7 +74,7 @@ const defaultSearch = '*'; const emptySearch = ''; export function getDefaultStepDefineState( - kibanaContext: KibanaContextValue + kibanaContext: InitializedKibanaContextValue ): StepDefineExposedState { return { aggList: {} as PivotAggsConfigDict, @@ -83,13 +82,9 @@ export function getDefaultStepDefineState( isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, searchString: - isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined - ? kibanaContext.combinedQuery - : defaultSearch, + kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch, searchQuery: - isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined - ? kibanaContext.combinedQuery - : defaultSearch, + kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch, sourceConfigUpdated: false, valid: false, }; @@ -195,8 +190,8 @@ interface Props { onChange(s: StepDefineExposedState): void; } -export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange }) => { - const kibanaContext = useContext(KibanaContext); +export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange }) => { + const kibanaContext = useKibanaContext(); const defaults = { ...getDefaultStepDefineState(kibanaContext), ...overrides }; @@ -224,10 +219,6 @@ export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange // The list of selected group by fields const [groupByList, setGroupByList] = useState(defaults.groupByList); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - const indexPattern = kibanaContext.currentIndexPattern; const { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index bb900766483df..30c447f62c760 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, Fragment, FC } from 'react'; +import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { AggListSummary } from '../aggregation_list'; import { GroupByListSummary } from '../group_by_list'; @@ -35,11 +35,7 @@ export const StepDefineSummary: FC = ({ groupByList, aggList, }) => { - const kibanaContext = useContext(KibanaContext); - - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } + const kibanaContext = useKibanaContext(); const pivotQuery = getPivotQuery(searchQuery); let useCodeBlock = false; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx index 0843dfc306f85..824ff0462cbba 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import ReactDOM from 'react-dom'; import { SimpleQuery } from '../../../../common'; @@ -23,7 +23,7 @@ interface TestHookProps { callback: Callback; } -const TestHook: SFC = ({ callback }) => { +const TestHook: FC = ({ callback }) => { callback(); return null; }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts index 92e3bdded4f6a..e02f2473fc10b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts @@ -32,7 +32,8 @@ interface EsMappingType { type: ES_FIELD_TYPES; } -type PreviewData = Array>; +export type PreviewItem = Dictionary; +type PreviewData = PreviewItem[]; interface PreviewMappings { properties: Dictionary; } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index aa917ca3b05ec..a01481fde343c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { metadata } from 'ui/metadata'; @@ -13,7 +13,7 @@ import { toastNotifications } from 'ui/notify'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; import { ToastNotificationText } from '../../../../components'; @@ -54,8 +54,8 @@ interface Props { onChange(s: StepDetailsExposedState): void; } -export const StepDetailsForm: SFC = React.memo(({ overrides = {}, onChange }) => { - const kibanaContext = useContext(KibanaContext); +export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange }) => { + const kibanaContext = useKibanaContext(); const defaults = { ...getDefaultStepDetailsState(), ...overrides }; @@ -80,56 +80,47 @@ export const StepDetailsForm: SFC = React.memo(({ overrides = {}, onChang useEffect(() => { // use an IIFE to avoid returning a Promise to useEffect. (async function() { - if (isKibanaContextInitialized(kibanaContext)) { - try { - setTransformIds( - (await api.getTransforms()).transforms.map( - (transform: TransformPivotConfig) => transform.id - ) - ); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { - defaultMessage: 'An error occurred getting the existing transform IDs:', - }), - text: toMountPoint(), - }); - } + try { + setTransformIds( + (await api.getTransforms()).transforms.map( + (transform: TransformPivotConfig) => transform.id + ) + ); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { + defaultMessage: 'An error occurred getting the existing transform IDs:', + }), + text: toMountPoint(), + }); + } - try { - setIndexNames((await api.getIndices()).map(index => index.name)); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { - defaultMessage: 'An error occurred getting the existing index names:', - }), - text: toMountPoint(), - }); - } + try { + setIndexNames((await api.getIndices()).map(index => index.name)); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { + defaultMessage: 'An error occurred getting the existing index names:', + }), + text: toMountPoint(), + }); + } - try { - setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles()); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles', - { - defaultMessage: 'An error occurred getting the existing index pattern titles:', - } - ), - text: toMountPoint(), - }); - } + try { + setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles()); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles', { + defaultMessage: 'An error occurred getting the existing index pattern titles:', + }), + text: toMountPoint(), + }); } })(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps }, [kibanaContext.initialized]); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - const dateFieldNames = kibanaContext.currentIndexPattern.fields .filter(f => f.type === 'date') .map(f => f.name) diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx index ddb34dfa3f87d..7d4d25c1d05cf 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -12,7 +12,7 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { StepDetailsExposedState } from './step_details_form'; -export const StepDetailsSummary: SFC = React.memo( +export const StepDetailsSummary: FC = React.memo( ({ continuousModeDateField, createIndexPattern, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index d5b32fc7f05af..109cf81da6caa 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useContext, useEffect, useRef, useState } from 'react'; +import React, { Fragment, FC, useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { getCreateRequestBody } from '../../../../common'; @@ -42,7 +42,7 @@ interface DefinePivotStepProps { setStepDefineState: React.Dispatch>; } -const StepDefine: SFC = ({ +const StepDefine: FC = ({ isCurrentStep, stepDefineState, setCurrentStep, @@ -67,8 +67,8 @@ const StepDefine: SFC = ({ ); }; -export const Wizard: SFC = React.memo(() => { - const kibanaContext = useContext(KibanaContext); +export const Wizard: FC = React.memo(() => { + const kibanaContext = useKibanaContext(); // The current WIZARD_STEP const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE); @@ -108,11 +108,6 @@ export const Wizard: SFC = React.memo(() => { } }, []); - if (!isKibanaContextInitialized(kibanaContext)) { - // TODO proper loading indicator - return null; - } - const indexPattern = kibanaContext.currentIndexPattern; const transformConfig = getCreateRequestBody( @@ -134,18 +129,6 @@ export const Wizard: SFC = React.memo(() => { ); - // scroll to the currently selected wizard step - /* - function scrollToRef() { - if (definePivotRef !== null && definePivotRef.current !== null) { - // TODO Fix types - const dummy = definePivotRef as any; - const headerOffset = 70; - window.scrollTo(0, dummy.current.offsetTop - headerOffset); - } - } - */ - const stepsConfig = [ { title: i18n.translate('xpack.transform.transformsWizard.stepDefineTitle', { @@ -171,7 +154,6 @@ export const Wizard: SFC = React.memo(() => { { setCurrentStep(WIZARD_STEPS.DEFINE); - // scrollToRef(); }} next={() => setCurrentStep(WIZARD_STEPS.CREATE)} nextActive={stepDetailsState.valid} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx index bfdfcbf696d35..35ca789f4daaf 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -17,7 +17,7 @@ interface StepsNavProps { next?(): void; } -export const WizardNav: SFC = ({ +export const WizardNav: FC = ({ previous, previousActive = true, next, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index 2214d1f5adfff..f63f3b6d6e7be 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -26,7 +26,7 @@ import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/cons import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; -import { KibanaProvider } from '../../lib/kibana'; +import { KibanaProvider, RenderOnlyWithInitializedKibanaContext } from '../../lib/kibana'; import { Wizard } from './components/wizard'; @@ -82,7 +82,9 @@ export const CreateTransformSection: FC = ({ match }) => { - + + + diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap index 40ad836ad9969..1f134cd39948b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap @@ -90,7 +90,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = ] } />, - "id": "transform-details", + "data-test-subj": "transformDetailsTab", + "id": "transform-details-tab-fq_date_histogram_1m_1441", "name": "Transform details", } } @@ -188,7 +189,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = ] } />, - "id": "transform-details", + "data-test-subj": "transformDetailsTab", + "id": "transform-details-tab-fq_date_histogram_1m_1441", "name": "Transform details", }, Object { @@ -229,14 +231,16 @@ exports[`Transform: Transform List Minimal initialization 1`] = } } />, - "id": "transform-json", + "data-test-subj": "transformJsonTab", + "id": "transform-json-tab-fq_date_histogram_1m_1441", "name": "JSON", }, Object { "content": , - "id": "transform-messages", + "data-test-subj": "transformMessagesTab", + "id": "transform-messages-tab-fq_date_histogram_1m_1441", "name": "Messages", }, Object { @@ -277,7 +281,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = } } />, - "id": "transform-preview", + "data-test-subj": "transformPreviewTab", + "id": "transform-preview-tab-fq_date_histogram_1m_1441", "name": "Preview", }, ] diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap index b55a1c410d687..39964399f66db 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap @@ -1,40 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Job List Expanded Row Minimal initialization 1`] = ` - - + + - -
+ +
+ + - - - + +
`; exports[`Transform: Job List Expanded Row
Minimal initialization 1`] = ` diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap index 0d4a80a94ee51..dea6f57bcaab0 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap @@ -1,22 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Transform List Expanded Row Minimal initialization 1`] = ` - - - - + + + + Minimal \\"version\\": \\"8.0.0\\", \\"create_time\\": 1564388146667 }" - /> - - -   - - + /> + + +   + + +
`; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index 19ab74cc9ed85..050dedbc8e0b4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -88,14 +88,14 @@ export const getColumns = ( const columns: [ ExpanderColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - ComputedColumnType, - ComputedColumnType, - ComputedColumnType, - ActionsColumnType + FieldDataColumnType, + FieldDataColumnType, + FieldDataColumnType, + FieldDataColumnType, + ComputedColumnType, + ComputedColumnType, + ComputedColumnType, + ActionsColumnType ] = [ { align: RIGHT_ALIGNMENT, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 24d09c899580e..c02b7e9ce5b1b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { EuiTabbedContent } from '@elastic/eui'; @@ -35,7 +35,7 @@ interface Props { item: TransformListRow; } -export const ExpandedRow: SFC = ({ item }) => { +export const ExpandedRow: FC = ({ item }) => { const stateValues = { ...item.stats }; delete stateValues.stats; delete stateValues.checkpointing; @@ -121,7 +121,8 @@ export const ExpandedRow: SFC = ({ item }) => { const tabs = [ { - id: 'transform-details', + id: `transform-details-tab-${item.id}`, + 'data-test-subj': 'transformDetailsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformSettingsLabel', { @@ -131,12 +132,14 @@ export const ExpandedRow: SFC = ({ item }) => { content: , }, { - id: 'transform-json', + id: `transform-json-tab-${item.id}`, + 'data-test-subj': 'transformJsonTab', name: 'JSON', content: , }, { - id: 'transform-messages', + id: `transform-messages-tab-${item.id}`, + 'data-test-subj': 'transformMessagesTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel', { @@ -146,7 +149,8 @@ export const ExpandedRow: SFC = ({ item }) => { content: , }, { - id: 'transform-preview', + id: `transform-preview-tab-${item.id}`, + 'data-test-subj': 'transformPreviewTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel', { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx index 7ba05a5fe41ec..527033c46b469 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC } from 'react'; +import React, { Fragment, FC } from 'react'; import { EuiDescriptionList, @@ -29,7 +29,7 @@ interface SectionProps { section: SectionConfig; } -export const Section: SFC = ({ section }) => { +export const Section: FC = ({ section }) => { if (section.items.length === 0) { return null; } @@ -48,29 +48,31 @@ interface ExpandedRowDetailsPaneProps { sections: SectionConfig[]; } -export const ExpandedRowDetailsPane: SFC = ({ sections }) => { +export const ExpandedRowDetailsPane: FC = ({ sections }) => { return ( - - - {sections - .filter(s => s.position === 'left') - .map(s => ( - - -
- - ))} - - - {sections - .filter(s => s.position === 'right') - .map(s => ( - - -
- - ))} - - +
+ + + {sections + .filter(s => s.position === 'left') + .map(s => ( + + +
+ + ))} + + + {sections + .filter(s => s.position === 'right') + .map(s => ( + + +
+ + ))} + + +
); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx index 416d93007daba..6792f4b80f665 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { // @ts-ignore @@ -18,20 +18,22 @@ interface Props { json: object; } -export const ExpandedRowJsonPane: SFC = ({ json }) => { +export const ExpandedRowJsonPane: FC = ({ json }) => { return ( - - - - - -   - +
+ + + + + +   + +
); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index 526152a829963..1aeb93c162847 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore @@ -22,7 +22,7 @@ interface Props { transformId: string; } -export const ExpandedRowMessagesPane: React.SFC = ({ transformId }) => { +export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -143,7 +143,7 @@ export const ExpandedRowMessagesPane: React.SFC = ({ transformId }) => { }; return ( - +
= ({ transformId }) => { pagination={pagination} onChange={onChange} /> - +
); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 68f2964d41e57..5a5e8308b8d57 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -14,12 +14,13 @@ import { useApi } from '../../../../hooks/use_api'; import { getFlattenedFields, useRefreshTransformList, + EsDoc, PreviewRequestBody, TransformPivotConfig, } from '../../../../common'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { TransformTable } from './transform_table'; +import { transformTableFactory } from './transform_table'; interface Props { transformConfig: TransformPivotConfig; @@ -45,12 +46,14 @@ function getDataFromTransform( transformConfig: TransformPivotConfig ): { previewRequest: PreviewRequestBody; groupByArr: string[] | [] } { const index = transformConfig.source.index; + const query = transformConfig.source.query; const pivot = transformConfig.pivot; const groupByArr = []; const previewRequest: PreviewRequestBody = { source: { index, + query, }, pivot, }; @@ -67,8 +70,8 @@ function getDataFromTransform( } export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const [previewData, setPreviewData] = useState([]); - const [columns, setColumns] = useState([]); + const [previewData, setPreviewData] = useState([]); + const [columns, setColumns] = useState> | []>([]); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState(''); @@ -97,8 +100,8 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const columnKeys = getFlattenedFields(resp.preview[0]); columnKeys.sort(sortColumns(groupByArr)); - const tableColumns: FieldDataColumnType[] = columnKeys.map(k => { - const column: FieldDataColumnType = { + const tableColumns: Array> = columnKeys.map(k => { + const column: FieldDataColumnType = { field: k, name: k, sortable: true, @@ -191,17 +194,27 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { setSortDirection(direction); }; + const transformTableLoading = previewData.length === 0 && isLoading === true; + const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`; + + const TransformTable = transformTableFactory(); + return ( - +
+ ({ + 'data-test-subj': 'transformPreviewTabContentRow', + })} + sorting={sorting} + error={errorMessage} + /> +
); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 02abadb85fbd0..e1a65f631df3c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -42,7 +42,7 @@ import { StopAction } from './action_stop'; import { ItemIdToExpandedRowMap, Query, Clause } from './common'; import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; -import { ProgressBar, TransformTable } from './transform_table'; +import { ProgressBar, transformTableFactory } from './transform_table'; function getItemIdToExpandedRowMap( itemIds: TransformId[], @@ -374,6 +374,8 @@ export const TransformList: FC = ({ onSelectionChange: (selected: TransformListRow[]) => setTransformSelection(selected), }; + const TransformTable = transformTableFactory(); + return (
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx index cd6f6654a8e9e..8c7920c124bef 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx @@ -11,7 +11,7 @@ import React, { Fragment } from 'react'; import { EuiProgress } from '@elastic/eui'; -import { MlInMemoryTableBasic } from '../../../../../shared_imports'; +import { mlInMemoryTableBasicFactory } from '../../../../../shared_imports'; // The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement // of the table and doesn't play well with auto-refreshing. That's why we're displaying @@ -73,32 +73,35 @@ const getInitialSorting = (columns: any, sorting: any) => { }; }; -export class TransformTable extends MlInMemoryTableBasic { - static getDerivedStateFromProps(nextProps: any, prevState: any) { - const derivedState = { - ...prevState.prevProps, - pageIndex: nextProps.pagination.initialPageIndex, - pageSize: nextProps.pagination.initialPageSize, - }; +export function transformTableFactory() { + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return class TransformTable extends MlInMemoryTableBasic { + static getDerivedStateFromProps(nextProps: any, prevState: any) { + const derivedState = { + ...prevState.prevProps, + pageIndex: nextProps.pagination.initialPageIndex, + pageSize: nextProps.pagination.initialPageSize, + }; - if (nextProps.items !== prevState.prevProps.items) { - Object.assign(derivedState, { - prevProps: { - items: nextProps.items, - }, - }); - } + if (nextProps.items !== prevState.prevProps.items) { + Object.assign(derivedState, { + prevProps: { + items: nextProps.items, + }, + }); + } - const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - Object.assign(derivedState, { - sortName, - sortDirection, - }); + const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); + if ( + sortName !== prevState.prevProps.sortName || + sortDirection !== prevState.prevProps.sortDirection + ) { + Object.assign(derivedState, { + sortName, + sortDirection, + }); + } + return derivedState; } - return derivedState; - } + }; } diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 35bcac16921b9..fe9fe8b8e695b 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -25,12 +25,12 @@ export { ExpanderColumnType, FieldDataColumnType, ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, OnTableChangeArg, SortingPropType, SortDirection, SORT_DIRECTION, -} from '../../ml/public/components/ml_in_memory_table'; +} from '../../ml/public/application/components/ml_in_memory_table'; // @ts-ignore: could not find declaration file for module -export { KqlFilterBar } from '../../ml/public/components/kql_filter_bar'; +export { KqlFilterBar } from '../../ml/public/application/components/kql_filter_bar'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts b/x-pack/legacy/plugins/upgrade_assistant/common/types.ts index 60018173781a0..ce653e461e13b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/common/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import { SavedObject, SavedObjectAttributes } from 'src/core/server'; export enum ReindexStep { @@ -79,15 +78,6 @@ export interface UIReindex { stop: boolean; } -export interface UpgradeAssistantTelemetryServer extends Legacy.Server { - usage: { - collectorSet: { - makeUsageCollector: any; - register: any; - }; - }; -} - export interface UpgradeAssistantTelemetrySavedObject { ui_open: { overview: number; diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts index 7c38fbf02a564..1be728d263372 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/index.ts @@ -3,18 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import Joi from 'joi'; import { Legacy } from 'kibana'; import { resolve } from 'path'; import mappings from './mappings.json'; -import { initServer } from './server'; +import { plugin } from './server/np_ready'; export function upgradeAssistant(kibana: any) { - return new kibana.Plugin({ + const publicSrc = resolve(__dirname, 'public'); + const npSrc = resolve(publicSrc, 'np_ready'); + + const config: Legacy.PluginSpecOptions = { id: 'upgrade_assistant', configPrefix: 'xpack.upgrade_assistant', require: ['elasticsearch'], uiExports: { + // @ts-ignore managementSections: ['plugins/upgrade_assistant'], savedObjectSchemas: { 'upgrade-assistant-reindex-operation': { @@ -24,10 +29,10 @@ export function upgradeAssistant(kibana: any) { isNamespaceAgnostic: true, }, }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), + styleSheetPaths: resolve(npSrc, 'application/index.scss'), mappings, }, - publicDir: resolve(__dirname, 'public'), + publicDir: publicSrc, config() { return Joi.object({ @@ -37,7 +42,31 @@ export function upgradeAssistant(kibana: any) { init(server: Legacy.Server) { // Add server routes and initialize the plugin here - initServer(server); + const instance = plugin({} as any); + const { usageCollection } = server.newPlatform.setup.plugins; + instance.setup(server.newPlatform.setup.core, { + usageCollection, + __LEGACY: { + // Legacy objects + events: server.events, + savedObjects: server.savedObjects, + + // Legacy functions + log: server.log.bind(server), + + // Legacy plugins + plugins: { + elasticsearch: server.plugins.elasticsearch, + xpack_main: server.plugins.xpack_main, + cloud: { + config: { + isCloudEnabled: _.get(server.plugins, 'cloud.config.isCloudEnabled', false), + }, + }, + }, + }, + }); }, - }); + }; + return new kibana.Plugin(config); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/app.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/app.tsx deleted file mode 100644 index 3bf3375c43bd9..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/app.tsx +++ /dev/null @@ -1,32 +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 React from 'react'; - -import { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { NEXT_MAJOR_VERSION } from '../common/version'; -import { UpgradeAssistantTabs } from './components/tabs'; - -export const RootComponent: React.StatelessComponent = () => ( -
- - - -

- -

-
-
-
- -
-); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/_index.scss deleted file mode 100644 index 8026031922301..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './tabs/checkup/index'; -@import './tabs/overview/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/_index.scss deleted file mode 100644 index 430d1e0aedf7b..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './deprecations/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/_index.scss deleted file mode 100644 index e370aeac0dfa2..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/_index.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import './cell'; -@import './reindex/index'; - -.upgDeprecations { - // Pull the container through the padding of EuiPageContent - margin-left: -$euiSizeL; - margin-right: -$euiSizeL; -} - -.upgDeprecations__item { - padding: $euiSize $euiSizeL; - border-top: $euiBorderThin; - - &:last-of-type { - margin-bottom: -$euiSizeL; - } -} - -.upgDeprecations__itemName { - font-weight: $euiFontWeightMedium; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx deleted file mode 100644 index cb38b848b3bd7..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx +++ /dev/null @@ -1,111 +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 React, { StatelessComponent } from 'react'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis'; -import { GroupByOption } from '../../../types'; - -import { COLOR_MAP, LEVEL_MAP } from '../constants'; -import { DeprecationCell } from './cell'; -import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table'; - -const sortByLevelDesc = (a: DeprecationInfo, b: DeprecationInfo) => { - return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]); -}; - -/** - * Used to show a single deprecation message with any detailed information. - */ -const MessageDeprecation: StatelessComponent<{ deprecation: EnrichedDeprecationInfo }> = ({ - deprecation, -}) => { - const items = []; - - if (deprecation.details) { - items.push({ body: deprecation.details }); - } - - return ( - - ); -}; - -/** - * Used to show a single (simple) deprecation message with any detailed information. - */ -const SimpleMessageDeprecation: StatelessComponent<{ deprecation: EnrichedDeprecationInfo }> = ({ - deprecation, -}) => { - const items = []; - - if (deprecation.details) { - items.push({ body: deprecation.details }); - } - - return ; -}; - -interface IndexDeprecationProps { - deprecation: DeprecationInfo; - indices: IndexDeprecationDetails[]; -} - -/** - * Shows a single deprecation and table of affected indices with details for each index. - */ -const IndexDeprecation: StatelessComponent = ({ deprecation, indices }) => { - return ( - - - - ); -}; - -/** - * A list of deprecations that is either shown as individual deprecation cells or as a - * deprecation summary for a list of indices. - */ -export const DeprecationList: StatelessComponent<{ - deprecations: EnrichedDeprecationInfo[]; - currentGroupBy: GroupByOption; -}> = ({ deprecations, currentGroupBy }) => { - // If we're grouping by message and the first deprecation has an index field, show an index - // group deprecation. Otherwise, show each message. - if (currentGroupBy === GroupByOption.message && deprecations[0].index !== undefined) { - // We assume that every deprecation message is the same issue (since they have the same - // message) and that each deprecation will have an index associated with it. - const indices = deprecations.map(dep => ({ - index: dep.index!, - details: dep.details, - reindex: dep.reindex === true, - })); - return ; - } else if (currentGroupBy === GroupByOption.index) { - return ( -
- {deprecations.sort(sortByLevelDesc).map(dep => ( - - ))} -
- ); - } else { - return ( -
- {deprecations.sort(sortByLevelDesc).map(dep => ( - - ))} -
- ); - } -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/_index.scss deleted file mode 100644 index 2d52575cffbbb..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './button'; -@import './flyout/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/_index.scss deleted file mode 100644 index f695ae175feca..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './step_progress'; -@import './warnings_step'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/filter_bar.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/filter_bar.tsx deleted file mode 100644 index 0921b5e7e5cfa..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/filter_bar.tsx +++ /dev/null @@ -1,65 +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 { groupBy } from 'lodash'; -import React from 'react'; - -import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { LevelFilterOption } from '../../types'; - -const LocalizedOptions: { [option: string]: string } = { - all: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.filterBar.allButtonLabel', { - defaultMessage: 'all', - }), - critical: i18n.translate( - 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel', - { defaultMessage: 'critical' } - ), -}; - -const allFilterOptions = Object.keys(LevelFilterOption) as LevelFilterOption[]; - -interface FilterBarProps { - allDeprecations?: DeprecationInfo[]; - currentFilter: LevelFilterOption; - onFilterChange(level: LevelFilterOption): void; -} - -export const FilterBar: React.StatelessComponent = ({ - allDeprecations = [], - currentFilter, - onFilterChange, -}) => { - const levelGroups = groupBy(allDeprecations, 'level'); - const levelCounts = Object.keys(levelGroups).reduce((counts, level) => { - counts[level] = levelGroups[level].length; - return counts; - }, {} as { [level: string]: number }); - - const allCount = allDeprecations.length; - - return ( - - - {allFilterOptions.map(option => ( - - {LocalizedOptions[option]} - - ))} - - - ); -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/_index.scss deleted file mode 100644 index a6bbc6ba13298..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './steps'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/index.tsx deleted file mode 100644 index 834f5f4afe5f6..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/index.tsx +++ /dev/null @@ -1,72 +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 React, { Fragment, StatelessComponent } from 'react'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageContent, - EuiPageContentBody, - EuiSpacer, - // @ts-ignore - EuiStat, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { NEXT_MAJOR_VERSION } from '../../../../common/version'; -import { LoadingErrorBanner } from '../../error_banner'; -import { LoadingState, UpgradeAssistantTabProps } from '../../types'; -import { Steps } from './steps'; - -export const OverviewTab: StatelessComponent = props => ( - - - - -

- -

-
- - - - {props.alertBanner && ( - - {props.alertBanner} - - - - )} - - - - {props.loadingState === LoadingState.Success && } - - {props.loadingState === LoadingState.Loading && ( - - - - - - )} - - {props.loadingState === LoadingState.Error && ( - - )} - - -
-); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/types.ts b/x-pack/legacy/plugins/upgrade_assistant/public/components/types.ts deleted file mode 100644 index dc31308a1ea34..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/types.ts +++ /dev/null @@ -1,50 +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 React from 'react'; - -import { - EnrichedDeprecationInfo, - UpgradeAssistantStatus, -} from '../../server/lib/es_migration_apis'; - -export interface UpgradeAssistantTabProps { - alertBanner?: React.ReactNode; - checkupData?: UpgradeAssistantStatus; - deprecations?: EnrichedDeprecationInfo[]; - refreshCheckupData: () => Promise; - loadingError?: Error; - loadingState: LoadingState; - setSelectedTabIndex: (tabIndex: number) => void; -} - -// eslint-disable-next-line react/prefer-stateless-function -export class UpgradeAssistantTabComponent< - T extends UpgradeAssistantTabProps = UpgradeAssistantTabProps, - S = {} -> extends React.Component {} - -export enum LoadingState { - Loading, - Success, - Error, -} - -export enum LevelFilterOption { - all = 'all', - critical = 'critical', -} - -export enum GroupByOption { - message = 'message', - index = 'index', - node = 'node', -} - -export enum TelemetryState { - Running, - Complete, -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/index.scss deleted file mode 100644 index 999cca93fcd7a..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/_styling_constants'; - -@import './components/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/index.ts b/x-pack/legacy/plugins/upgrade_assistant/public/index.ts new file mode 100644 index 0000000000000..d22b5d64b6b46 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './legacy'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/index.tsx deleted file mode 100644 index 2f631d3771ecb..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/index.tsx +++ /dev/null @@ -1,41 +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 { wrapInI18nContext } from 'ui/i18n'; -// @ts-ignore -import { management } from 'ui/management'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -// @ts-ignore -import routes from 'ui/routes'; -import { NEXT_MAJOR_VERSION } from '../common/version'; -import { RootComponent } from './app'; - -const BASE_PATH = `/management/elasticsearch/upgrade_assistant`; - -function startApp() { - management.getSection('elasticsearch').register('upgrade_assistant', { - visible: true, - display: i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${NEXT_MAJOR_VERSION}.0` }, - }), - order: 100, - url: `#${BASE_PATH}`, - }); - - uiModules.get('kibana').directive('upgradeAssistant', (reactDirective: any) => { - return reactDirective(wrapInI18nContext(RootComponent)); - }); - - routes.when(`${BASE_PATH}/:view?`, { - template: - '', - }); -} - -startApp(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts b/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts new file mode 100644 index 0000000000000..3d5144249dfef --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ComponentType } from 'react'; +import { i18n } from '@kbn/i18n'; + +/* LEGACY IMPORTS */ +import { npSetup } from 'ui/new_platform'; +import { wrapInI18nContext } from 'ui/i18n'; +import { management } from 'ui/management'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import routes from 'ui/routes'; +import chrome from 'ui/chrome'; +/* LEGACY IMPORTS */ + +import { NEXT_MAJOR_VERSION } from '../common/version'; +import { plugin } from './np_ready'; + +const BASE_PATH = `/management/elasticsearch/upgrade_assistant`; + +export interface LegacyAppMountParameters { + __LEGACY: { renderToElement: (RootComponent: ComponentType) => void }; +} + +export interface LegacyApp { + mount(ctx: any, params: LegacyAppMountParameters): void; +} + +export interface LegacyManagementPlugin { + sections: { + get( + name: string + ): { + registerApp(app: LegacyApp): void; + }; + }; +} + +// Based on /rfcs/text/0006_management_section_service.md +export interface LegacyPlugins { + management: LegacyManagementPlugin; + __LEGACY: { + XSRF: string; + isCloudEnabled: boolean; + }; +} + +function startApp() { + routes.when(`${BASE_PATH}/:view?`, { + template: + '', + }); + + const legacyPluginsShim: LegacyPlugins = { + __LEGACY: { + XSRF: chrome.getXsrfToken(), + isCloudEnabled: chrome.getInjected('isCloudEnabled', false), + }, + management: { + sections: { + get(_: string) { + return { + registerApp(app) { + management.getSection('elasticsearch').register('upgrade_assistant', { + visible: true, + display: i18n.translate('xpack.upgradeAssistant.appTitle', { + defaultMessage: '{version} Upgrade Assistant', + values: { version: `${NEXT_MAJOR_VERSION}.0` }, + }), + order: 100, + url: `#${BASE_PATH}`, + }); + + app.mount( + {}, + { + __LEGACY: { + // While there is not an NP API for registering management section apps yet + renderToElement: RootComponent => { + uiModules + .get('kibana') + .directive('upgradeAssistant', (reactDirective: any) => { + return reactDirective(wrapInI18nContext(RootComponent)); + }); + }, + }, + } + ); + }, + }; + }, + }, + }, + }; + + const pluginInstance = plugin(); + + pluginInstance.setup(npSetup.core, legacyPluginsShim); +} + +startApp(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx new file mode 100644 index 0000000000000..571967ab114c9 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NEXT_MAJOR_VERSION } from '../../../common/version'; +import { UpgradeAssistantTabs } from './components/tabs'; +import { AppContextProvider, ContextValue, AppContext } from './app_context'; + +type AppDependencies = ContextValue; + +export const RootComponent = (deps: AppDependencies) => { + return ( + +
+ + + +

+ +

+
+
+
+ + {({ http }) => } + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx new file mode 100644 index 0000000000000..a48a4efa3bbdf --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'src/core/public'; +import React, { createContext, useContext } from 'react'; + +export interface ContextValue { + http: HttpSetup; + isCloudEnabled: boolean; + XSRF: string; +} + +export const AppContext = createContext({} as any); + +export const AppContextProvider = ({ + children, + value, +}: { + children: React.ReactNode; + value: ContextValue; +}) => { + return {children}; +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('useAppContext must be called from inside AppContext'); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss new file mode 100644 index 0000000000000..bb01107f334f6 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss @@ -0,0 +1,2 @@ +@import 'tabs/checkup/index'; +@import 'tabs/overview/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/error_banner.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/error_banner.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx index 527f2b6486d7f..06cd5809c951a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/error_banner.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { UpgradeAssistantTabProps } from './types'; -export const LoadingErrorBanner: React.StatelessComponent> = ({ loadingError }) => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/latest_minor_banner.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/latest_minor_banner.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx index 1f24377eba6cd..864df292fbffe 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/latest_minor_banner.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../common/version'; -export const LatestMinorBanner: React.StatelessComponent = () => ( +export const LatestMinorBanner: React.FunctionComponent = () => ( ({ get: jest.fn(), create: jest.fn(), @@ -23,6 +21,13 @@ import { OverviewTab } from './tabs/overview'; // Used to wait for promises to resolve and renders to finish before reading updates const promisesToResolve = () => new Promise(resolve => setTimeout(resolve, 0)); +const mockHttp = { + basePath: { + prepend: () => 'test', + }, + fetch() {}, +}; + describe('UpgradeAssistantTabs', () => { test('renders loading state', async () => { // @ts-ignore @@ -31,7 +36,7 @@ describe('UpgradeAssistantTabs', () => { /* never resolve */ }) ); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); // Should pass down loading status to child component expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Loading); }); @@ -44,7 +49,7 @@ describe('UpgradeAssistantTabs', () => { indices: [], }, }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); expect(axios.get).toHaveBeenCalled(); await promisesToResolve(); wrapper.update(); @@ -55,7 +60,7 @@ describe('UpgradeAssistantTabs', () => { test('network failure', async () => { // @ts-ignore axios.get.mockRejectedValue(new Error(`oh no!`)); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); // Should pass down error status to child component @@ -65,7 +70,7 @@ describe('UpgradeAssistantTabs', () => { it('upgrade error', async () => { // @ts-ignore axios.get.mockRejectedValue({ response: { status: 426 } }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); // Should display an informative message if the cluster is currently mid-upgrade diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx similarity index 93% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx index 76cc1e33ca06b..0b154fb20404d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx @@ -16,11 +16,9 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { HttpSetup } from 'src/core/public'; -import chrome from 'ui/chrome'; -import { kfetch } from 'ui/kfetch'; - -import { UpgradeAssistantStatus } from '../../server/lib/es_migration_apis'; +import { UpgradeAssistantStatus } from '../../../../server/np_ready/lib/es_migration_apis'; import { LatestMinorBanner } from './latest_minor_banner'; import { CheckupTab } from './tabs/checkup'; import { OverviewTab } from './tabs/overview'; @@ -41,13 +39,11 @@ interface TabsState { clusterUpgradeState: ClusterUpgradeState; } -export class UpgradeAssistantTabsUI extends React.Component< - ReactIntl.InjectedIntlProps, - TabsState -> { - constructor(props: ReactIntl.InjectedIntlProps) { - super(props); +type Props = ReactIntl.InjectedIntlProps & { http: HttpSetup }; +export class UpgradeAssistantTabsUI extends React.Component { + constructor(props: Props) { + super(props); this.state = { loadingState: LoadingState.Loading, clusterUpgradeState: ClusterUpgradeState.needsUpgrade, @@ -157,7 +153,9 @@ export class UpgradeAssistantTabsUI extends React.Component< private loadData = async () => { try { this.setState({ loadingState: LoadingState.Loading }); - const resp = await axios.get(chrome.addBasePath('/api/upgrade_assistant/status')); + const resp = await axios.get( + this.props.http.basePath.prepend('/api/upgrade_assistant/status') + ); this.setState({ loadingState: LoadingState.Success, checkupData: resp.data, @@ -246,8 +244,7 @@ export class UpgradeAssistantTabsUI extends React.Component< this.setState({ telemetryState: TelemetryState.Running }); - await kfetch({ - pathname: '/api/upgrade_assistant/telemetry/ui_open', + await this.props.http.fetch('/api/upgrade_assistant/telemetry/ui_open', { method: 'PUT', body: JSON.stringify(set({}, tabName, true)), }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/__fixtures__/checkup_api_response.json b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/__fixtures__/checkup_api_response.json rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss new file mode 100644 index 0000000000000..d64400a8abdcf --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss @@ -0,0 +1 @@ +@import 'deprecations/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx index 1805cb49e6ee6..9ba5441604ddc 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx @@ -11,8 +11,6 @@ import { LoadingState } from '../../types'; import AssistanceData from '../__fixtures__/checkup_api_response.json'; import { CheckupTab } from './checkup_tab'; -jest.mock('ui/kfetch'); - const defaultProps = { checkupLabel: 'index', deprecations: AssistanceData.indices, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx index 5b8fd21f9c1fc..9aa40663125ae 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/checkup_tab.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../../common/version'; +import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; import { GroupByOption, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/constants.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/constants.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/controls.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/controls.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx index 5f3691f668103..bfa440139866d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/controls.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { StatelessComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; @@ -26,7 +26,7 @@ interface CheckupControlsProps extends ReactIntl.InjectedIntlProps { onGroupByChange: (groupBy: GroupByOption) => void; } -export const CheckupControlsUI: StatelessComponent = ({ +export const CheckupControlsUI: FunctionComponent = ({ allDeprecations, loadingState, loadData, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/_cell.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/_cell.scss rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss new file mode 100644 index 0000000000000..445ef6269afb9 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss @@ -0,0 +1,18 @@ +.upgDeprecations { + // Pull the container through the padding of EuiPageContent + margin-left: -$euiSizeL; + margin-right: -$euiSizeL; +} + +.upgDeprecations__item { + padding: $euiSize $euiSizeL; + border-top: $euiBorderThin; + + &:last-of-type { + margin-bottom: -$euiSizeL; + } +} + +.upgDeprecations__itemName { + font-weight: $euiFontWeightMedium; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss new file mode 100644 index 0000000000000..55aff6b379db5 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss @@ -0,0 +1,3 @@ +@import 'cell'; +@import 'deprecations'; +@import 'reindex/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx similarity index 84% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx index 29a0076d00d0e..4bd2f7c4bf62c 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, StatelessComponent } from 'react'; +import React, { ReactNode, FunctionComponent } from 'react'; import { EuiFlexGroup, @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ReindexButton } from './reindex'; +import { AppContext } from '../../../../app_context'; interface DeprecationCellProps { items?: Array<{ title?: string; body: string }>; @@ -30,7 +31,7 @@ interface DeprecationCellProps { /** * Used to display a deprecation with links to docs, a health indicator, and other descriptive information. */ -export const DeprecationCell: StatelessComponent = ({ +export const DeprecationCell: FunctionComponent = ({ headline, healthColor, reindexIndexName, @@ -77,7 +78,11 @@ export const DeprecationCell: StatelessComponent = ({ {reindexIndexName && ( - + + {({ http, XSRF }) => ( + + )} + )} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/count_summary.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx similarity index 85% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/count_summary.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx index a66244b0e7886..a0e55dc55c865 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/count_summary.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx @@ -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 React, { Fragment, StatelessComponent } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; -export const DeprecationCountSummary: StatelessComponent<{ +export const DeprecationCountSummary: FunctionComponent<{ deprecations: EnrichedDeprecationInfo[]; allDeprecations: EnrichedDeprecationInfo[]; }> = ({ deprecations, allDeprecations }) => ( diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx index 6c541742a539b..28f5f6894b78f 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -11,12 +11,10 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EuiBadge, EuiPagination } from '@elastic/eui'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; -jest.mock('ui/kfetch'); - describe('filterDeps', () => { test('filters on levels', () => { const fd = filterDeps(LevelFilterOption.critical); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx index 9ce0b8a8cefd3..74f66b6c4fb35 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx @@ -5,7 +5,7 @@ */ import { groupBy } from 'lodash'; -import React, { Fragment, StatelessComponent } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiAccordion, @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationCountSummary } from './count_summary'; @@ -59,7 +59,7 @@ export const filterDeps = (level: LevelFilterOption, search: string = '') => { /** * A single accordion item for a grouped deprecation item. */ -export const DeprecationAccordion: StatelessComponent<{ +export const DeprecationAccordion: FunctionComponent<{ id: string; deprecations: EnrichedDeprecationInfo[]; title: string; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/health.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx similarity index 91% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/health.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx index ab4daece767a0..63c32e0026505 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/health.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx @@ -5,7 +5,7 @@ */ import { countBy } from 'lodash'; -import React, { StatelessComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -36,7 +36,7 @@ interface DeprecationHealthProps { single?: boolean; } -const SingleHealth: StatelessComponent<{ level: DeprecationInfo['level']; label: string }> = ({ +const SingleHealth: FunctionComponent<{ level: DeprecationInfo['level']; label: string }> = ({ level, label, }) => ( @@ -52,7 +52,7 @@ const SingleHealth: StatelessComponent<{ level: DeprecationInfo['level']; label: * Displays a summary health for a list of deprecations that shows the number and level of severity * deprecations in the list. */ -export const DeprecationHealth: StatelessComponent = ({ +export const DeprecationHealth: FunctionComponent = ({ deprecations, single = false, }) => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx index 040459309f728..8c211704c7aff 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx @@ -9,8 +9,6 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { IndexDeprecationTableProps, IndexDeprecationTableUI } from './index_table'; -jest.mock('ui/kfetch'); - describe('IndexDeprecationTable', () => { const defaultProps = { indices: [ diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx similarity index 92% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx index 5a1deb59c270e..eba906edc0509 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; import { ReindexButton } from './reindex'; +import { AppContext } from '../../../../app_context'; const PAGE_SIZES = [10, 25, 50, 100, 250, 500, 1000]; @@ -143,7 +144,13 @@ export class IndexDeprecationTableUI extends React.Component< actions: [ { render(indexDep: IndexDeprecationDetails) { - return ; + return ( + + {({ XSRF, http }) => ( + + )} + + ); }, }, ], diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx index 6be89f411f580..78ded73593464 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx @@ -7,12 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; import { GroupByOption } from '../../../types'; import { DeprecationList } from './list'; -jest.mock('ui/kfetch'); - describe('DeprecationList', () => { describe('group by message', () => { const defaultProps = { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx new file mode 100644 index 0000000000000..15a3d94974dcd --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FunctionComponent } from 'react'; + +import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { GroupByOption } from '../../../types'; + +import { COLOR_MAP, LEVEL_MAP } from '../constants'; +import { DeprecationCell } from './cell'; +import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table'; + +const sortByLevelDesc = (a: DeprecationInfo, b: DeprecationInfo) => { + return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]); +}; + +/** + * Used to show a single deprecation message with any detailed information. + */ +const MessageDeprecation: FunctionComponent<{ deprecation: EnrichedDeprecationInfo }> = ({ + deprecation, +}) => { + const items = []; + + if (deprecation.details) { + items.push({ body: deprecation.details }); + } + + return ( + + ); +}; + +/** + * Used to show a single (simple) deprecation message with any detailed information. + */ +const SimpleMessageDeprecation: FunctionComponent<{ deprecation: EnrichedDeprecationInfo }> = ({ + deprecation, +}) => { + const items = []; + + if (deprecation.details) { + items.push({ body: deprecation.details }); + } + + return ; +}; + +interface IndexDeprecationProps { + deprecation: DeprecationInfo; + indices: IndexDeprecationDetails[]; +} + +/** + * Shows a single deprecation and table of affected indices with details for each index. + */ +const IndexDeprecation: FunctionComponent = ({ deprecation, indices }) => { + return ( + + + + ); +}; + +/** + * A list of deprecations that is either shown as individual deprecation cells or as a + * deprecation summary for a list of indices. + */ +export const DeprecationList: FunctionComponent<{ + deprecations: EnrichedDeprecationInfo[]; + currentGroupBy: GroupByOption; +}> = ({ deprecations, currentGroupBy }) => { + // If we're grouping by message and the first deprecation has an index field, show an index + // group deprecation. Otherwise, show each message. + if (currentGroupBy === GroupByOption.message && deprecations[0].index !== undefined) { + // We assume that every deprecation message is the same issue (since they have the same + // message) and that each deprecation will have an index associated with it. + const indices = deprecations.map(dep => ({ + index: dep.index!, + details: dep.details, + reindex: dep.reindex === true, + })); + return ; + } else if (currentGroupBy === GroupByOption.index) { + return ( +
+ {deprecations.sort(sortByLevelDesc).map(dep => ( + + ))} +
+ ); + } else { + return ( +
+ {deprecations.sort(sortByLevelDesc).map(dep => ( + + ))} +
+ ); + } +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/_button.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/_button.scss rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss new file mode 100644 index 0000000000000..014edc96b0565 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss @@ -0,0 +1,2 @@ +@import 'button'; +@import 'flyout/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/button.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx index 6addd3dae642a..2a28018a3ae81 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -10,14 +10,16 @@ import { Subscription } from 'rxjs'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { kfetch } from 'ui/kfetch'; -import { ReindexStatus, UIReindexOption } from '../../../../../../common/types'; +import { HttpSetup } from 'src/core/public'; +import { ReindexStatus, UIReindexOption } from '../../../../../../../../common/types'; import { LoadingState } from '../../../../types'; import { ReindexFlyout } from './flyout'; import { ReindexPollingService, ReindexState } from './polling_service'; interface ReindexButtonProps { indexName: string; + xsrf: string; + http: HttpSetup; } interface ReindexButtonState { @@ -152,7 +154,8 @@ export class ReindexButton extends React.Component { /** * Displays a flyout that shows the current reindexing status for a given index. */ -export const ChecklistFlyoutStep: React.StatelessComponent<{ +export const ChecklistFlyoutStep: React.FunctionComponent<{ closeFlyout: () => void; reindexState: ReindexState; startReindex: () => void; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/container.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/container.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/index.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx index d2ef6446962b7..48aa1228797f7 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../../../common/types'; import { ReindexState } from '../polling_service'; import { ReindexProgress } from './progress'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx index 0510752b04e9a..ed9ac07ed6179 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx @@ -16,12 +16,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../../../common/types'; import { LoadingState } from '../../../../../types'; import { ReindexState } from '../polling_service'; import { StepProgress, StepProgressStep } from './step_progress'; -const ErrorCallout: React.StatelessComponent<{ errorMessage: string | null }> = ({ +const ErrorCallout: React.FunctionComponent<{ errorMessage: string | null }> = ({ errorMessage, }) => ( @@ -38,7 +38,7 @@ const PausedCallout = () => ( /> ); -const ReindexProgressBar: React.StatelessComponent<{ +const ReindexProgressBar: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; }> = ({ @@ -113,7 +113,7 @@ const orderedSteps = Object.values(ReindexStep).sort() as number[]; * Displays a list of steps in the reindex operation, the current status, a progress bar, * and any error messages that are encountered. */ -export const ReindexProgress: React.StatelessComponent<{ +export const ReindexProgress: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; }> = props => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx similarity index 92% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx index 78537b16cc9c6..8b0bb348941e0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx @@ -11,7 +11,7 @@ import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused' | 'cancelled'; -const StepStatus: React.StatelessComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => { +const StepStatus: React.FunctionComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => { if (status === 'incomplete') { return {idx + 1}.; } else if (status === 'inProgress') { @@ -45,7 +45,7 @@ const StepStatus: React.StatelessComponent<{ status: STATUS; idx: number }> = ({ throw new Error(`Unsupported status: ${status}`); }; -const Step: React.StatelessComponent = ({ +const Step: React.FunctionComponent = ({ title, status, children, @@ -79,7 +79,7 @@ export interface StepProgressStep { /** * A generic component that displays a series of automated steps and the system's progress. */ -export const StepProgress: React.StatelessComponent<{ +export const StepProgress: React.FunctionComponent<{ steps: StepProgressStep[]; }> = ({ steps }) => { return ( diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx index 0ba13fc630c6e..227f377fcdbef 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx @@ -8,7 +8,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { mount, shallow } from 'enzyme'; import React from 'react'; -import { ReindexWarning } from '../../../../../../../common/types'; +import { ReindexWarning } from '../../../../../../../../../common/types'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; describe('WarningsFlyoutStep', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx index 1465812c4803d..91e35c0bd7dc0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReindexWarning } from '../../../../../../../common/types'; +import { ReindexWarning } from '../../../../../../../../../common/types'; interface CheckedIds { [id: string]: boolean; @@ -29,7 +29,7 @@ interface CheckedIds { export const idForWarning = (warning: ReindexWarning) => `reindexWarning-${warning}`; -const WarningCheckbox: React.StatelessComponent<{ +const WarningCheckbox: React.FunctionComponent<{ checkedIds: CheckedIds; warning: ReindexWarning; label: React.ReactNode; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/index.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts similarity index 87% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts index f70822aef102c..dc7a758839fe5 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReindexStatus, ReindexStep } from '../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; export const mockClient = { post: jest.fn().mockResolvedValue({ diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index 9f47f51bc8cde..cb2a0856f0f2e 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -6,8 +6,9 @@ import { mockClient } from './polling_service.test.mocks'; -import { ReindexStatus, ReindexStep } from '../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; +import { httpServiceMock } from 'src/core/public/http/http_service.mock'; describe('ReindexPollingService', () => { beforeEach(() => { @@ -24,7 +25,11 @@ describe('ReindexPollingService', () => { }, }); - const service = new ReindexPollingService('myIndex'); + const service = new ReindexPollingService( + 'myIndex', + 'myXsrf', + httpServiceMock.createSetupContract() + ); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -45,7 +50,11 @@ describe('ReindexPollingService', () => { }, }); - const service = new ReindexPollingService('myIndex'); + const service = new ReindexPollingService( + 'myIndex', + 'myXsrf', + httpServiceMock.createSetupContract() + ); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -66,7 +75,11 @@ describe('ReindexPollingService', () => { }, }); - const service = new ReindexPollingService('myIndex'); + const service = new ReindexPollingService( + 'myIndex', + 'myXsrf', + httpServiceMock.createSetupContract() + ); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -76,7 +89,11 @@ describe('ReindexPollingService', () => { describe('startReindex', () => { it('posts to endpoint', async () => { - const service = new ReindexPollingService('myIndex'); + const service = new ReindexPollingService( + 'myIndex', + 'myXsrf', + httpServiceMock.createSetupContract() + ); await service.startReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex'); @@ -85,7 +102,11 @@ describe('ReindexPollingService', () => { describe('cancelReindex', () => { it('posts to cancel endpoint', async () => { - const service = new ReindexPollingService('myIndex'); + const service = new ReindexPollingService( + 'myIndex', + 'myXsrf', + httpServiceMock.createSetupContract() + ); await service.cancelReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex/cancel'); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts similarity index 82% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.ts rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts index 3977a169f4aca..879fafe610982 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/reindex/polling_service.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts @@ -3,31 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; -import chrome from 'ui/chrome'; +import axios, { AxiosInstance } from 'axios'; import { BehaviorSubject } from 'rxjs'; + +import { HttpSetup } from 'src/core/public'; import { IndexGroup, ReindexOperation, ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../../../common/types'; +} from '../../../../../../../../common/types'; import { LoadingState } from '../../../../types'; const POLL_INTERVAL = 1000; -const XSRF = chrome.getXsrfToken(); - -export const APIClient = axios.create({ - headers: { - Accept: 'application/json', - credentials: 'same-origin', - 'Content-Type': 'application/json', - 'kbn-version': XSRF, - 'kbn-xsrf': XSRF, - }, -}); export interface ReindexState { loadingState: LoadingState; @@ -55,13 +45,24 @@ interface StatusResponse { export class ReindexPollingService { public status$: BehaviorSubject; private pollTimeout?: NodeJS.Timeout; + private APIClient: AxiosInstance; - constructor(private indexName: string) { + constructor(private indexName: string, private xsrf: string, private http: HttpSetup) { this.status$ = new BehaviorSubject({ loadingState: LoadingState.Loading, errorMessage: null, reindexTaskPercComplete: null, }); + + this.APIClient = axios.create({ + headers: { + Accept: 'application/json', + credentials: 'same-origin', + 'Content-Type': 'application/json', + 'kbn-version': this.xsrf, + 'kbn-xsrf': this.xsrf, + }, + }); } public updateStatus = async () => { @@ -69,8 +70,8 @@ export class ReindexPollingService { this.stopPolling(); try { - const { data } = await APIClient.get( - chrome.addBasePath(`/api/upgrade_assistant/reindex/${this.indexName}`) + const { data } = await this.APIClient.get( + this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) ); this.updateWithResponse(data); @@ -106,8 +107,8 @@ export class ReindexPollingService { errorMessage: null, cancelLoadingState: undefined, }); - const { data } = await APIClient.post( - chrome.addBasePath(`/api/upgrade_assistant/reindex/${this.indexName}`) + const { data } = await this.APIClient.post( + this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) ); this.updateWithResponse({ reindexOp: data }); @@ -124,8 +125,8 @@ export class ReindexPollingService { cancelLoadingState: LoadingState.Loading, }); - await APIClient.post( - chrome.addBasePath(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`) + await this.APIClient.post( + this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`) ); } catch (e) { this.status$.next({ diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/filter_bar.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/filter_bar.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx new file mode 100644 index 0000000000000..de2527a493b91 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { groupBy } from 'lodash'; +import React from 'react'; + +import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +import { LevelFilterOption } from '../../types'; + +const LocalizedOptions: { [option: string]: string } = { + all: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.filterBar.allButtonLabel', { + defaultMessage: 'all', + }), + critical: i18n.translate( + 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel', + { defaultMessage: 'critical' } + ), +}; + +const allFilterOptions = Object.keys(LevelFilterOption) as LevelFilterOption[]; + +interface FilterBarProps { + allDeprecations?: DeprecationInfo[]; + currentFilter: LevelFilterOption; + onFilterChange(level: LevelFilterOption): void; +} + +export const FilterBar: React.FunctionComponent = ({ + allDeprecations = [], + currentFilter, + onFilterChange, +}) => { + const levelGroups = groupBy(allDeprecations, 'level'); + const levelCounts = Object.keys(levelGroups).reduce((counts, level) => { + counts[level] = levelGroups[level].length; + return counts; + }, {} as { [level: string]: number }); + + const allCount = allDeprecations.length; + + return ( + + + {allFilterOptions.map(option => ( + + {LocalizedOptions[option]} + + ))} + + + ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/group_by_bar.test.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/group_by_bar.test.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/group_by_bar.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/group_by_bar.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx index 6b1053faf6b6d..564236024372b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/group_by_bar.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx @@ -26,7 +26,7 @@ interface GroupByBarProps { onGroupByChange: (groupBy: GroupByOption) => void; } -export const GroupByBar: React.StatelessComponent = ({ +export const GroupByBar: React.FunctionComponent = ({ availableGroupByOptions, currentGroupBy, onGroupByChange, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/index.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss new file mode 100644 index 0000000000000..c64a8f5a5326d --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss @@ -0,0 +1 @@ +@import 'steps'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/_steps.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/_steps.scss rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx similarity index 87% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/deprecation_logging_toggle.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx index 8d107331eb65f..97eb284c7b771 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx @@ -10,19 +10,25 @@ import React from 'react'; import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; +import { HttpSetup } from 'src/core/public'; + import { LoadingState } from '../../types'; +interface DeprecationLoggingTabProps extends ReactIntl.InjectedIntlProps { + xsrf: string; + http: HttpSetup; +} + interface DeprecationLoggingTabState { loadingState: LoadingState; loggingEnabled?: boolean; } export class DeprecationLoggingToggleUI extends React.Component< - ReactIntl.InjectedIntlProps, + DeprecationLoggingTabProps, DeprecationLoggingTabState > { - constructor(props: ReactIntl.InjectedIntlProps) { + constructor(props: DeprecationLoggingTabProps) { super(props); this.state = { @@ -83,7 +89,7 @@ export class DeprecationLoggingToggleUI extends React.Component< try { this.setState({ loadingState: LoadingState.Loading }); const resp = await axios.get( - chrome.addBasePath('/api/upgrade_assistant/deprecation_logging') + this.props.http.basePath.prepend('/api/upgrade_assistant/deprecation_logging') ); this.setState({ loadingState: LoadingState.Success, @@ -96,18 +102,19 @@ export class DeprecationLoggingToggleUI extends React.Component< private toggleLogging = async () => { try { + const { http, xsrf } = this.props; // Optimistically toggle the UI const newEnabled = !this.state.loggingEnabled; this.setState({ loadingState: LoadingState.Loading, loggingEnabled: newEnabled }); const resp = await axios.put( - chrome.addBasePath('/api/upgrade_assistant/deprecation_logging'), + http.basePath.prepend('/api/upgrade_assistant/deprecation_logging'), { isEnabled: newEnabled, }, { headers: { - 'kbn-xsrf': chrome.getXsrfToken(), + 'kbn-xsrf': xsrf, }, } ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx new file mode 100644 index 0000000000000..0a05743d1623d --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FunctionComponent } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + // @ts-ignore + EuiStat, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { LoadingErrorBanner } from '../../error_banner'; +import { LoadingState, UpgradeAssistantTabProps } from '../../types'; +import { Steps } from './steps'; + +export const OverviewTab: FunctionComponent = props => ( + + + + +

+ +

+
+ + + + {props.alertBanner && ( + + {props.alertBanner} + + + + )} + + + + {props.loadingState === LoadingState.Success && } + + {props.loadingState === LoadingState.Loading && ( + + + + + + )} + + {props.loadingState === LoadingState.Error && ( + + )} + + +
+); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/steps.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/steps.tsx rename to x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx index d43a86d2b0e06..ccba51c73c136 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/overview/steps.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, StatelessComponent } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiFormRow, @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../../common/version'; import { UpgradeAssistantTabProps } from '../../types'; import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; +import { useAppContext } from '../../../app_context'; // Leaving these here even if unused so they are picked up for i18n static analysis // Keep this until last minor release (when next major is also released). @@ -54,7 +54,7 @@ const WAIT_FOR_RELEASE_STEP = { // Swap in this step for the one above it on the last minor release. // @ts-ignore -const START_UPGRADE_STEP = { +const START_UPGRADE_STEP = (isCloudEnabled: boolean) => ({ title: i18n.translate('xpack.upgradeAssistant.overviewTab.steps.startUpgradeStep.stepTitle', { defaultMessage: 'Start your upgrade', }), @@ -62,7 +62,7 @@ const START_UPGRADE_STEP = {

- {chrome.getInjected('isCloudEnabled', false) ? ( + {isCloudEnabled ? ( ), -}; +}); -export const StepsUI: StatelessComponent = ({ checkupData, setSelectedTabIndex, intl }) => { +export const StepsUI: FunctionComponent = ({ + checkupData, + setSelectedTabIndex, + intl, +}) => { const checkupDataTyped = (checkupData! as unknown) as { [checkupType: string]: any[] }; const countByType = Object.keys(checkupDataTyped).reduce((counts, checkupType) => { counts[checkupType] = checkupDataTyped[checkupType].length; return counts; }, {} as { [checkupType: string]: number }); + // Uncomment when START_UPGRADE_STEP is in use! + const { http, XSRF /* , isCloudEnabled */ } = useAppContext(); + return ( - + ), @@ -264,6 +270,7 @@ export const StepsUI: StatelessComponent ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts new file mode 100644 index 0000000000000..2d9a373f20b7e --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { + EnrichedDeprecationInfo, + UpgradeAssistantStatus, +} from '../../../../server/np_ready/lib/es_migration_apis'; + +export interface UpgradeAssistantTabProps { + alertBanner?: React.ReactNode; + checkupData?: UpgradeAssistantStatus; + deprecations?: EnrichedDeprecationInfo[]; + refreshCheckupData: () => Promise; + loadingError?: Error; + loadingState: LoadingState; + setSelectedTabIndex: (tabIndex: number) => void; +} + +// eslint-disable-next-line react/prefer-stateless-function +export class UpgradeAssistantTabComponent< + T extends UpgradeAssistantTabProps = UpgradeAssistantTabProps, + S = {} +> extends React.Component {} + +export enum LoadingState { + Loading, + Success, + Error, +} + +export enum LevelFilterOption { + all = 'all', + critical = 'critical', +} + +export enum GroupByOption { + message = 'message', + index = 'index', + node = 'node', +} + +export enum TelemetryState { + Running, + Complete, +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss new file mode 100644 index 0000000000000..6000af5498cd6 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss @@ -0,0 +1,3 @@ +@import 'src/legacy/ui/public/styles/_styling_constants'; + +@import 'components/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts new file mode 100644 index 0000000000000..020d6972f8280 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UpgradeAssistantUIPlugin } from './plugin'; + +export const plugin = () => { + return new UpgradeAssistantUIPlugin(); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..16a0c9632fb25 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'src/core/public'; +import { RootComponent } from './application/app'; +import { LegacyPlugins } from '../legacy'; + +export class UpgradeAssistantUIPlugin implements Plugin { + async setup( + { http }: CoreSetup, + { management, __LEGACY: { XSRF, isCloudEnabled } }: LegacyPlugins + ) { + const appRegistrar = management.sections.get('kibana'); + return appRegistrar.registerApp({ + mount(__, { __LEGACY: { renderToElement } }) { + return renderToElement(() => RootComponent({ http, XSRF, isCloudEnabled })); + }, + }); + } + async start() {} + async stop() {} +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/index.ts index 5fcdbe136a4f2..8b0704283509d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/index.ts @@ -4,31 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; -import { UpgradeAssistantTelemetryServer } from '../common/types'; -import { credentialStoreFactory } from './lib/reindexing/credential_store'; -import { makeUpgradeAssistantUsageCollector } from './lib/telemetry'; -import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; -import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; -import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; -import { registerTelemetryRoutes } from './routes/telemetry'; - -export function initServer(server: Legacy.Server) { - registerClusterCheckupRoutes(server); - registerDeprecationLoggingRoutes(server); - - // The ReindexWorker uses a map of request headers that contain the authentication credentials - // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not - // want to expose these credentials to any unauthenticated users. We also want to avoid any need - // to add a user for a special index just for upgrading. This in-memory cache allows us to - // process jobs without the browser staying on the page, but will require that jobs go into - // a paused state if no Kibana nodes have the required credentials. - const credentialStore = credentialStoreFactory(); - - const worker = registerReindexWorker(server, credentialStore); - registerReindexIndicesRoutes(server, worker, credentialStore); - - // Bootstrap the needed routes and the collector for the telemetry - registerTelemetryRoutes(server as UpgradeAssistantTelemetryServer); - makeUpgradeAssistantUsageCollector(server as UpgradeAssistantTelemetryServer); -} +export { plugin } from './np_ready'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts deleted file mode 100644 index 9a0fca6d4139c..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts +++ /dev/null @@ -1,125 +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 { SemVer } from 'semver'; -import { CURRENT_VERSION } from '../../common/version'; -import { - EsVersionPrecheck, - getAllNodeVersions, - verifyAllMatchKibanaVersion, -} from './es_version_precheck'; - -describe('getAllNodeVersions', () => { - it('returns a list of unique node versions', async () => { - const callCluster = jest.fn().mockResolvedValue({ - nodes: { - node1: { version: '7.0.0' }, - node2: { version: '7.0.0' }, - node3: { version: '6.0.0' }, - }, - }); - - await expect(getAllNodeVersions(callCluster)).resolves.toEqual([ - new SemVer('6.0.0'), - new SemVer('7.0.0'), - ]); - }); -}); - -describe('verifyAllMatchKibanaVersion', () => { - it('throws if any are higher version', () => { - expect(() => - verifyAllMatchKibanaVersion([new SemVer('99999.0.0')]) - ).toThrowErrorMatchingInlineSnapshot( - `"There are some nodes running a different version of Elasticsearch"` - ); - }); - - it('throws if any are lower version', () => { - expect(() => - verifyAllMatchKibanaVersion([new SemVer('0.0.0')]) - ).toThrowErrorMatchingInlineSnapshot( - `"There are some nodes running a different version of Elasticsearch"` - ); - }); - - it('does not throw if all are same major', () => { - const versions = [ - CURRENT_VERSION, - CURRENT_VERSION.inc('minor'), - CURRENT_VERSION.inc('minor').inc('minor'), - ]; - - expect(() => verifyAllMatchKibanaVersion(versions)).not.toThrow(); - }); -}); - -describe('EsVersionPrecheck', () => { - it('throws a 403 when callCluster fails with a 403', async () => { - const fakeCallWithRequest = jest.fn().mockRejectedValue({ status: 403 }); - const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest })); - const fakeRequest = { - server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } }, - } as any; - - await expect(EsVersionPrecheck.method(fakeRequest, {} as any)).rejects.toHaveProperty( - 'output.statusCode', - 403 - ); - }); - - it('throws a 426 message w/ allNodesUpgraded = false when nodes are not on same version', async () => { - const fakeCallWithRequest = jest.fn().mockResolvedValue({ - nodes: { - node1: { version: CURRENT_VERSION.raw }, - node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, - }, - }); - const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest })); - const fakeRequest = { - server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } }, - } as any; - - const result = EsVersionPrecheck.method(fakeRequest, {} as any); - await expect(result).rejects.toHaveProperty('output.statusCode', 426); - await expect(result).rejects.toHaveProperty( - 'output.payload.attributes.allNodesUpgraded', - false - ); - }); - - it('throws a 426 message w/ allNodesUpgraded = true when nodes are on next version', async () => { - const fakeCallWithRequest = jest.fn().mockResolvedValue({ - nodes: { - node1: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, - node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, - }, - }); - const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest })); - const fakeRequest = { - server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } }, - } as any; - - const result = EsVersionPrecheck.method(fakeRequest, {} as any); - await expect(result).rejects.toHaveProperty('output.statusCode', 426); - await expect(result).rejects.toHaveProperty('output.payload.attributes.allNodesUpgraded', true); - }); - - it('returns true when nodes are on same version', async () => { - const fakeCallWithRequest = jest.fn().mockResolvedValue({ - nodes: { - node1: { version: CURRENT_VERSION.raw }, - node2: { version: CURRENT_VERSION.raw }, - }, - }); - const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest })); - const fakeRequest = { - server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } }, - } as any; - - await expect(EsVersionPrecheck.method(fakeRequest, {} as any)).resolves.toBe(true); - }); -}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.ts b/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.ts deleted file mode 100644 index d84d5f5444472..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_version_precheck.ts +++ /dev/null @@ -1,75 +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 Boom from 'boom'; -import { Request, RouteOptionsPreObject } from 'hapi'; -import { uniq } from 'lodash'; -import { SemVer } from 'semver'; - -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { CURRENT_VERSION } from '../../common/version'; - -/** - * Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster. - * @param request - */ -export const getAllNodeVersions = async (callCluster: CallCluster) => { - // Get the version information for all nodes in the cluster. - const { nodes } = (await callCluster('nodes.info', { - filterPath: 'nodes.*.version', - })) as { nodes: { [nodeId: string]: { version: string } } }; - - const versionStrings = Object.values(nodes).map(({ version }) => version); - - return uniq(versionStrings) - .sort() - .map(version => new SemVer(version)); -}; - -export const verifyAllMatchKibanaVersion = (allNodeVersions: SemVer[]) => { - // Determine if all nodes in the cluster are running the same major version as Kibana. - const numDifferentVersion = allNodeVersions.filter( - esNodeVersion => esNodeVersion.major !== CURRENT_VERSION.major - ).length; - const numSameVersion = allNodeVersions.filter( - esNodeVersion => esNodeVersion.major === CURRENT_VERSION.major - ).length; - - if (numDifferentVersion) { - const error = new Boom(`There are some nodes running a different version of Elasticsearch`, { - // 426 means "Upgrade Required" and is used when semver compatibility is not met. - statusCode: 426, - }); - - error.output.payload.attributes = { allNodesUpgraded: !numSameVersion }; - throw error; - } -}; - -export const EsVersionPrecheck = { - assign: 'esVersionCheck', - async method(request: Request) { - const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('admin'); - const callCluster = callWithRequest.bind(callWithRequest, request) as CallCluster; - - let allNodeVersions: SemVer[]; - - try { - allNodeVersions = await getAllNodeVersions(callCluster); - } catch (e) { - if (e.status === 403) { - throw Boom.forbidden(e.message); - } - - throw e; - } - - // This will throw if there is an issue - verifyAllMatchKibanaVersion(allNodeVersions); - - return true; - }, -} as RouteOptionsPreObject; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/index.ts deleted file mode 100644 index 7d1d734748a82..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { makeUpgradeAssistantUsageCollector } from './usage_collector'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts new file mode 100644 index 0000000000000..cf1b78e1e3920 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { PluginInitializerContext } from 'src/core/server'; +import { UpgradeAssistantServerPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => { + return new UpgradeAssistantServerPlugin(); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts index 2cf0a8f5020fd..199d389408442 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts @@ -5,8 +5,8 @@ */ import { get } from 'lodash'; -import { Legacy } from 'kibana'; import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; +import { RequestShim } from '../types'; interface DeprecationLoggingStatus { isEnabled: boolean; @@ -14,7 +14,7 @@ interface DeprecationLoggingStatus { export async function getDeprecationLoggingStatus( callWithRequest: CallClusterWithRequest, - req: Legacy.Request + req: RequestShim ): Promise { const response = await callWithRequest(req, 'cluster.getSettings', { includeDefaults: true, @@ -27,7 +27,7 @@ export async function getDeprecationLoggingStatus( export async function setDeprecationLogging( callWithRequest: CallClusterWithRequest, - req: Legacy.Request, + req: RequestShim, isEnabled: boolean ): Promise { const response = await callWithRequest(req, 'cluster.putSettings', { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/es_migration_apis.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts index 8ed85d19c411a..b52c4c374266f 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; - -import { Request } from 'hapi'; import { CallClusterWithRequest, DeprecationAPIResponse, DeprecationInfo, } from 'src/legacy/core_plugins/elasticsearch'; +import { RequestShim } from '../types'; + export interface EnrichedDeprecationInfo extends DeprecationInfo { index?: string; node?: string; @@ -27,7 +26,7 @@ export interface UpgradeAssistantStatus { export async function getUpgradeAssistantStatus( callWithRequest: CallClusterWithRequest, - req: Request, + req: RequestShim, isCloudEnabled: boolean ): Promise { const deprecations = await callWithRequest(req, 'transport.request', { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts new file mode 100644 index 0000000000000..bbabe557df4d4 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { SemVer } from 'semver'; +import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; +import { CURRENT_VERSION } from '../../../common/version'; +import { + esVersionCheck, + getAllNodeVersions, + verifyAllMatchKibanaVersion, +} from './es_version_precheck'; + +describe('getAllNodeVersions', () => { + it('returns a list of unique node versions', async () => { + const adminClient = ({ + callAsInternalUser: jest.fn().mockResolvedValue({ + nodes: { + node1: { version: '7.0.0' }, + node2: { version: '7.0.0' }, + node3: { version: '6.0.0' }, + }, + }), + } as unknown) as IScopedClusterClient; + + await expect(getAllNodeVersions(adminClient)).resolves.toEqual([ + new SemVer('6.0.0'), + new SemVer('7.0.0'), + ]); + }); +}); + +describe('verifyAllMatchKibanaVersion', () => { + it('detects higher version nodes', () => { + const result = verifyAllMatchKibanaVersion([new SemVer('99999.0.0')]); + expect(result.allNodesMatch).toBe(false); + expect(result.allNodesUpgraded).toBe(true); + }); + + it('detects lower version nodes', () => { + const result = verifyAllMatchKibanaVersion([new SemVer('0.0.0')]); + expect(result.allNodesMatch).toBe(false); + expect(result.allNodesUpgraded).toBe(true); + }); + + it('detects if all are on same major correctly', () => { + const versions = [ + CURRENT_VERSION, + CURRENT_VERSION.inc('minor'), + CURRENT_VERSION.inc('minor').inc('minor'), + ]; + + const result = verifyAllMatchKibanaVersion(versions); + expect(result.allNodesMatch).toBe(true); + expect(result.allNodesUpgraded).toBe(false); + }); + + it('detects partial matches', () => { + const versions = [ + new SemVer('0.0.0'), + CURRENT_VERSION.inc('minor'), + CURRENT_VERSION.inc('minor').inc('minor'), + ]; + + const result = verifyAllMatchKibanaVersion(versions); + expect(result.allNodesMatch).toBe(false); + expect(result.allNodesUpgraded).toBe(false); + }); +}); + +describe('EsVersionPrecheck', () => { + it('returns a 403 when callCluster fails with a 403', async () => { + const fakeCall = jest.fn().mockRejectedValue({ status: 403 }); + + const ctx = { + core: { + elasticsearch: { + adminClient: { + callAsInternalUser: fakeCall, + }, + }, + }, + } as any; + + const result = await esVersionCheck(ctx, kibanaResponseFactory); + expect(result).toHaveProperty('status', 403); + }); + + it('returns a 426 message w/ allNodesUpgraded = false when nodes are not on same version', async () => { + const ctx = { + core: { + elasticsearch: { + adminClient: { + callAsInternalUser: jest.fn().mockResolvedValue({ + nodes: { + node1: { version: CURRENT_VERSION.raw }, + node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + }, + }), + }, + }, + }, + } as any; + + const result = await esVersionCheck(ctx, kibanaResponseFactory); + expect(result).toHaveProperty('status', 426); + expect(result).toHaveProperty('payload.attributes.allNodesUpgraded', false); + }); + + it('returns a 426 message w/ allNodesUpgraded = true when nodes are on next version', async () => { + const ctx = { + core: { + elasticsearch: { + adminClient: { + callAsInternalUser: jest.fn().mockResolvedValue({ + nodes: { + node1: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw }, + }, + }), + }, + }, + }, + } as any; + + const result = await esVersionCheck(ctx, kibanaResponseFactory); + expect(result).toHaveProperty('status', 426); + expect(result).toHaveProperty('payload.attributes.allNodesUpgraded', true); + }); + + it('returns undefined when nodes are on same version', async () => { + const ctx = { + core: { + elasticsearch: { + adminClient: { + callAsInternalUser: jest.fn().mockResolvedValue({ + nodes: { + node1: { version: CURRENT_VERSION.raw }, + node2: { version: CURRENT_VERSION.raw }, + }, + }), + }, + }, + }, + } as any; + + await expect(esVersionCheck(ctx, kibanaResponseFactory)).resolves.toBe(undefined); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts new file mode 100644 index 0000000000000..2fb3effe43793 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { uniq } from 'lodash'; +import { SemVer } from 'semver'; +import { + IScopedClusterClient, + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; +import { CURRENT_VERSION } from '../../../common/version'; + +/** + * Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster. + */ +export const getAllNodeVersions = async (adminClient: IScopedClusterClient) => { + // Get the version information for all nodes in the cluster. + const { nodes } = (await adminClient.callAsInternalUser('nodes.info', { + filterPath: 'nodes.*.version', + })) as { nodes: { [nodeId: string]: { version: string } } }; + + const versionStrings = Object.values(nodes).map(({ version }) => version); + + return uniq(versionStrings) + .sort() + .map(version => new SemVer(version)); +}; + +export const verifyAllMatchKibanaVersion = (allNodeVersions: SemVer[]) => { + // Determine if all nodes in the cluster are running the same major version as Kibana. + const numDifferentVersion = allNodeVersions.filter( + esNodeVersion => esNodeVersion.major !== CURRENT_VERSION.major + ).length; + + const numSameVersion = allNodeVersions.filter( + esNodeVersion => esNodeVersion.major === CURRENT_VERSION.major + ).length; + + if (numDifferentVersion) { + return { + allNodesMatch: false, + // If Kibana is talking to nodes and none have the same major version as Kibana, they must a be of + // a higher major version. + allNodesUpgraded: numSameVersion === 0, + }; + } + return { + allNodesMatch: true, + allNodesUpgraded: false, + }; +}; + +/** + * This is intended as controller/handler level code so it knows about HTTP + */ +export const esVersionCheck = async ( + ctx: RequestHandlerContext, + response: KibanaResponseFactory +) => { + const { adminClient } = ctx.core.elasticsearch; + let allNodeVersions: SemVer[]; + + try { + allNodeVersions = await getAllNodeVersions(adminClient); + } catch (e) { + if (e.status === 403) { + return response.forbidden({ body: e.message }); + } + + throw e; + } + + const result = verifyAllMatchKibanaVersion(allNodeVersions); + if (!result.allNodesMatch) { + return response.customError({ + // 426 means "Upgrade Required" and is used when semver compatibility is not met. + statusCode: 426, + body: { + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: result.allNodesUpgraded, + }, + }, + }); + } +}; + +export const versionCheckHandlerWrapper = (handler: RequestHandler) => async ( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) => { + const errorResponse = await esVersionCheck(ctx, response); + if (errorResponse) { + return errorResponse; + } + return handler(ctx, request, response); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts index ce892df0de946..06fa755472238 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReindexSavedObject } from '../../../common/types'; +import { ReindexSavedObject } from '../../../../common/types'; import { Credential, credentialStoreFactory } from './credential_store'; describe('credentialStore', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts index 32f5ec9977b72..a051d16b5779f 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts @@ -8,7 +8,7 @@ import { createHash } from 'crypto'; import { Request } from 'hapi'; import stringify from 'json-stable-stringify'; -import { ReindexSavedObject } from '../../../common/types'; +import { ReindexSavedObject } from '../../../../common/types'; export type Credential = Request['headers']; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts index 9ec06b72f02e2..7b346cc87edf6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; import { generateNewIndexName, getReindexWarnings, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts index f6dc471d0945d..0b95bc628fbb4 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts @@ -5,8 +5,8 @@ */ import { flow, omit } from 'lodash'; -import { ReindexWarning } from '../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +import { ReindexWarning } from '../../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; import { FlatSettings } from './types'; export interface ParsedIndexName { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts index 4569fdfa33a83..3fb855958a5d0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts @@ -13,8 +13,8 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +} from '../../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions'; describe('ReindexActions', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts index a162186ff0059..6683f80c8e779 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsFindResponse, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsFindResponse, SavedObjectsClientContract } from 'kibana/server'; import { IndexGroup, REINDEX_OP_TYPE, @@ -15,7 +15,7 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../common/types'; +} from '../../../../common/types'; import { generateNewIndexName } from './index_settings'; import { FlatSettings } from './types'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts index 1216a8d2c4c24..9cd41c8cbe826 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts @@ -10,8 +10,8 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +} from '../../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; import { isMlIndex, isWatcherIndex, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts index 41a4552b722de..0e6095f98b6ff 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts @@ -8,14 +8,14 @@ import Boom from 'boom'; import { Server } from 'hapi'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; +import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../common/types'; +} from '../../../../common/types'; import { generateNewIndexName, getReindexWarnings, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/types.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/worker.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts index 669eea623851c..628a47be9f5e7 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/reindexing/worker.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts @@ -5,11 +5,11 @@ */ import { CallCluster, CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; import { Request, Server } from 'src/legacy/server/kbn_server'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; -import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; -import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; +import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; +import { ReindexSavedObject, ReindexStatus } from '../../../../common/types'; import { CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; import { ReindexService, reindexServiceFactory } from './reindex_service'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts index 94df444e2c1bb..5f95f6e9fd555 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; import { upsertUIOpenOption } from './es_ui_open_apis'; /** @@ -15,12 +15,6 @@ import { upsertUIOpenOption } from './es_ui_open_apis'; describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts index 7036dcc4ea1a7..b52b3b812b7f9 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts @@ -4,19 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import { UIOpen, UIOpenOption, UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetryServer, -} from '../../../common/types'; +} from '../../../../common/types'; +import { RequestShim, ServerShim } from '../../types'; -async function incrementUIOpenOptionCounter( - server: UpgradeAssistantTelemetryServer, - uiOpenOptionCounter: UIOpenOption -) { +async function incrementUIOpenOptionCounter(server: ServerShim, uiOpenOptionCounter: UIOpenOption) { const { getSavedObjectsRepository } = server.savedObjects; const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); const internalRepository = getSavedObjectsRepository(callWithInternalUser); @@ -28,10 +24,7 @@ async function incrementUIOpenOptionCounter( ); } -export async function upsertUIOpenOption( - server: UpgradeAssistantTelemetryServer, - req: Legacy.Request -): Promise { +export async function upsertUIOpenOption(server: ServerShim, req: RequestShim): Promise { const { overview, cluster, indices } = req.payload as UIOpen; if (overview) { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts index a8a78470aabbe..3f2c80f7d6b75 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; import { upsertUIReindexOption } from './es_ui_reindex_apis'; /** @@ -15,12 +15,6 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis'; describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts similarity index 86% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts index 3cb965523a80b..626d51b298e72 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import { UIReindex, UIReindexOption, UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetryServer, -} from '../../../common/types'; +} from '../../../../common/types'; +import { RequestShim, ServerShim } from '../../types'; async function incrementUIReindexOptionCounter( - server: UpgradeAssistantTelemetryServer, + server: ServerShim, uiOpenOptionCounter: UIReindexOption ) { const { getSavedObjectsRepository } = server.savedObjects; @@ -29,8 +28,8 @@ async function incrementUIReindexOptionCounter( } export async function upsertUIReindexOption( - server: UpgradeAssistantTelemetryServer, - req: Legacy.Request + server: ServerShim, + req: RequestShim ): Promise { const { close, open, start, stop } = req.payload as UIReindex; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts new file mode 100644 index 0000000000000..898da4ab0073b --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerUpgradeAssistantUsageCollector } from './usage_collector'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts similarity index 81% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts index f0553578b86c8..27a0eef0d16f6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as usageCollector from './usage_collector'; +import { registerUpgradeAssistantUsageCollector } from './usage_collector'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -16,15 +16,16 @@ describe('Upgrade Assistant Usage Collector', () => { let registerStub: any; let server: any; let callClusterStub: any; + let usageCollection: any; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - register: {}, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { @@ -55,20 +56,20 @@ describe('Upgrade Assistant Usage Collector', () => { }); }); - describe('makeUpgradeAssistantUsageCollector', () => { - it('should call collectorSet.register', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + describe('registerUpgradeAssistantUsageCollector', () => { + it('should registerCollector', () => { + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = upgrade-assistant', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry'); }); it('fetchUpgradeAssistantMetrics should return correct info', async () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch( callClusterStub ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts similarity index 87% rename from x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts rename to x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts index 3a9b11a57f070..99c0441063ce6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts @@ -7,14 +7,15 @@ import { set } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE, UpgradeAssistantTelemetry, UpgradeAssistantTelemetrySavedObject, UpgradeAssistantTelemetrySavedObjectAttributes, - UpgradeAssistantTelemetryServer, -} from '../../../common/types'; +} from '../../../../common/types'; +import { ServerShim } from '../../types'; import { isDeprecationLoggingEnabled } from '../es_deprecation_logging_apis'; async function getSavedObjectAttributesFromRepo( @@ -43,7 +44,7 @@ async function getDeprecationLoggingStatusValue(callCluster: any): Promise { const { getSavedObjectsRepository } = server.savedObjects; const savedObjectsRepository = getSavedObjectsRepository(callCluster); @@ -97,13 +98,15 @@ export async function fetchUpgradeAssistantMetrics( }; } -export function makeUpgradeAssistantUsageCollector(server: UpgradeAssistantTelemetryServer) { - const kbnServer = server as UpgradeAssistantTelemetryServer; - const upgradeAssistantUsageCollector = kbnServer.usage.collectorSet.makeUsageCollector({ +export function registerUpgradeAssistantUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerShim +) { + const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ type: UPGRADE_ASSISTANT_TYPE, isReady: () => true, fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server), }); - kbnServer.usage.collectorSet.register(upgradeAssistantUsageCollector); + usageCollection.registerCollector(upgradeAssistantUsageCollector); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts new file mode 100644 index 0000000000000..3d4247ffe70bb --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { ServerShim, ServerShimWithRouter } from './types'; +import { credentialStoreFactory } from './lib/reindexing/credential_store'; +import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; +import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; +import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; +import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; + +import { registerTelemetryRoutes } from './routes/telemetry'; + +export class UpgradeAssistantServerPlugin implements Plugin { + setup( + { http }: CoreSetup, + { __LEGACY, usageCollection }: { usageCollection: UsageCollectionSetup; __LEGACY: ServerShim } + ) { + const router = http.createRouter(); + const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; + registerClusterCheckupRoutes(shimWithRouter); + registerDeprecationLoggingRoutes(shimWithRouter); + + // The ReindexWorker uses a map of request headers that contain the authentication credentials + // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not + // want to expose these credentials to any unauthenticated users. We also want to avoid any need + // to add a user for a special index just for upgrading. This in-memory cache allows us to + // process jobs without the browser staying on the page, but will require that jobs go into + // a paused state if no Kibana nodes have the required credentials. + const credentialStore = credentialStoreFactory(); + + const worker = registerReindexWorker(__LEGACY, credentialStore); + registerReindexIndicesRoutes(shimWithRouter, worker, credentialStore); + + // Bootstrap the needed routes and the collector for the telemetry + registerTelemetryRoutes(shimWithRouter); + registerUpgradeAssistantUsageCollector(usageCollection, __LEGACY); + } + + start(core: CoreStart, plugins: any) {} + + stop(): void {} +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts new file mode 100644 index 0000000000000..d09a66dbb4326 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const createRequestMock = (opts?: { + headers?: any; + params?: Record; + payload?: Record; +}) => { + return Object.assign({ headers: {} }, opts || {}); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts new file mode 100644 index 0000000000000..3769bc389123e --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RequestHandler } from 'kibana/server'; + +/** + * Creates a very crude mock of the new platform router implementation. This enables use to test + * controller/handler logic without making HTTP requests to an actual server. This does not enable + * us to test whether our paths actual match, only the response codes of controllers given certain + * inputs. This should be replaced by a more wholistic solution (like functional tests) eventually. + * + * This also bypasses any validation installed on the route. + */ +export const createMockRouter = () => { + const paths: Record>> = {}; + + const assign = (method: string) => ( + { path }: { path: string }, + handler: RequestHandler + ) => { + paths[method] = { + ...(paths[method] || {}), + ...{ [path]: handler }, + }; + }; + + return { + getHandler({ method, pathPattern }: { method: string; pathPattern: string }) { + return paths[method][pathPattern]; + }, + get: assign('get'), + post: assign('post'), + put: assign('put'), + patch: assign('patch'), + }; +}; + +export type MockRouter = ReturnType; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts new file mode 100644 index 0000000000000..6afb9d2a5e935 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory } from 'src/core/server'; +import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +// Need to require to get mock on named export to work. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const MigrationApis = require('../lib/es_migration_apis'); +MigrationApis.getUpgradeAssistantStatus = jest.fn(); + +import { registerClusterCheckupRoutes } from './cluster_checkup'; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the es_migration_apis test. + */ +describe('cluster checkup API', () => { + afterEach(() => jest.clearAllMocks()); + + let mockRouter: MockRouter; + let serverShim: any; + let ctxMock: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + ctxMock = { + core: {}, + }; + serverShim = { + router: mockRouter, + plugins: { + cloud: { + config: { + isCloudEnabled: true, + }, + }, + elasticsearch: { + getCluster: () => ({ callWithRequest: jest.fn() } as any), + } as any, + }, + }; + registerClusterCheckupRoutes(serverShim); + }); + + describe('with cloud enabled', () => { + it('is provided to getUpgradeAssistantStatus', async () => { + const spy = jest.spyOn(MigrationApis, 'getUpgradeAssistantStatus'); + + MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ + cluster: [], + indices: [], + nodes: [], + }); + + await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + expect(spy.mock.calls[0][2]).toBe(true); + }); + }); + + describe('GET /api/upgrade_assistant/reindex/{indexName}.json', () => { + it('returns state', async () => { + MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ + cluster: [], + indices: [], + nodes: [], + }); + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(JSON.stringify(resp.payload)).toMatchInlineSnapshot( + `"{\\"cluster\\":[],\\"indices\\":[],\\"nodes\\":[]}"` + ); + }); + + it('returns an 403 error if it throws forbidden', async () => { + const e: any = new Error(`you can't go here!`); + e.status = 403; + + MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(e); + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(403); + }); + + it('returns an 500 error if it throws', async () => { + MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); + + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(500); + }); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts new file mode 100644 index 0000000000000..3cfa567755b0f --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { ServerShimWithRouter } from '../types'; +import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; + +import { createRequestShim } from './create_request_shim'; + +export function registerClusterCheckupRoutes(server: ServerShimWithRouter) { + const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); + const isCloudEnabled = _.get(server.plugins, 'cloud.config.isCloudEnabled', false); + + server.router.get( + { + path: '/api/upgrade_assistant/status', + validate: false, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + try { + return response.ok({ + body: await getUpgradeAssistantStatus(callWithRequest, reqShim, isCloudEnabled), + }); + } catch (e) { + if (e.status === 403) { + return response.forbidden(e.message); + } + + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts new file mode 100644 index 0000000000000..b1a5c8b72d0e0 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { RequestShim } from '../types'; + +export const createRequestShim = (req: KibanaRequest): RequestShim => { + return { + headers: req.headers as Record, + payload: req.body || (req as any).payload, + params: req.params, + }; +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts new file mode 100644 index 0000000000000..c488f999b538e --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory } from 'src/core/server'; +import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import { registerDeprecationLoggingRoutes } from './deprecation_logging'; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the es_deprecation_logging_apis test. + */ +describe('deprecation logging API', () => { + let mockRouter: MockRouter; + let serverShim: any; + let callWithRequest: any; + const ctxMock: any = {}; + + beforeEach(() => { + mockRouter = createMockRouter(); + callWithRequest = jest.fn(); + serverShim = { + router: mockRouter, + plugins: { + cloud: { + config: { + isCloudEnabled: true, + }, + }, + elasticsearch: { + getCluster: () => ({ callWithRequest } as any), + } as any, + }, + }; + registerDeprecationLoggingRoutes(serverShim); + }); + + describe('GET /api/upgrade_assistant/deprecation_logging', () => { + it('returns isEnabled', async () => { + callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'WARN' } } }); + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ isEnabled: true }); + }); + + it('returns an error if it throws', async () => { + callWithRequest.mockRejectedValue(new Error(`scary error!`)); + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(500); + }); + }); + + describe('PUT /api/upgrade_assistant/deprecation_logging', () => { + it('returns isEnabled', async () => { + callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'ERROR' } } }); + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.payload).toEqual({ isEnabled: false }); + }); + + it('returns an error if it throws', async () => { + callWithRequest.mockRejectedValue(new Error(`scary error!`)); + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(ctxMock, { body: { isEnabled: false } }, kibanaResponseFactory); + + expect(resp.status).toEqual(500); + }); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts new file mode 100644 index 0000000000000..7e19ef3fb6047 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { + getDeprecationLoggingStatus, + setDeprecationLogging, +} from '../lib/es_deprecation_logging_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { ServerShimWithRouter } from '../types'; +import { createRequestShim } from './create_request_shim'; + +export function registerDeprecationLoggingRoutes(server: ServerShimWithRouter) { + const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); + + server.router.get( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: false, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + try { + const result = await getDeprecationLoggingStatus(callWithRequest, reqShim); + return response.ok({ body: result }); + } catch (e) { + return response.internalError({ body: e }); + } + }) + ); + + server.router.put( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: { + body: schema.object({ + isEnabled: schema.boolean(), + }), + }, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + try { + const { isEnabled } = reqShim.payload as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(callWithRequest, reqShim, isEnabled), + }); + } catch (e) { + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts new file mode 100644 index 0000000000000..d520324239656 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from 'src/core/server'; +import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; + +const mockReindexService = { + hasRequiredPrivileges: jest.fn(), + detectReindexWarnings: jest.fn(), + getIndexGroup: jest.fn(), + createReindexOperation: jest.fn(), + findAllInProgressOperations: jest.fn(), + findReindexOperation: jest.fn(), + processNextStep: jest.fn(), + resumeReindexOperation: jest.fn(), + cancelReindexing: jest.fn(), +}; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +jest.mock('../lib/reindexing', () => { + return { + reindexServiceFactory: () => mockReindexService, + }; +}); + +import { + IndexGroup, + ReindexSavedObject, + ReindexStatus, + ReindexWarning, +} from '../../../common/types'; +import { credentialStoreFactory } from '../lib/reindexing/credential_store'; +import { registerReindexIndicesRoutes } from './reindex_indices'; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the es_migration_apis test. + */ +describe('reindex API', () => { + let serverShim: any; + let mockRouter: MockRouter; + let ctxMock: any; + + const credentialStore = credentialStoreFactory(); + const worker = { + includes: jest.fn(), + forceRefresh: jest.fn(), + } as any; + + beforeEach(() => { + ctxMock = { + core: { + savedObjects: savedObjectsClientMock.create(), + }, + }; + mockRouter = createMockRouter(); + serverShim = { + router: mockRouter, + plugins: { + xpack_main: { + info: jest.fn(), + }, + elasticsearch: { + getCluster: () => ({ callWithRequest: jest.fn() } as any), + } as any, + }, + }; + registerReindexIndicesRoutes(serverShim, worker, credentialStore); + + mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); + mockReindexService.detectReindexWarnings.mockReset(); + mockReindexService.getIndexGroup.mockReset(); + mockReindexService.createReindexOperation.mockReset(); + mockReindexService.findAllInProgressOperations.mockReset(); + mockReindexService.findReindexOperation.mockReset(); + mockReindexService.processNextStep.mockReset(); + mockReindexService.resumeReindexOperation.mockReset(); + mockReindexService.cancelReindexing.mockReset(); + worker.includes.mockReset(); + worker.forceRefresh.mockReset(); + + // Reset the credentialMap + credentialStore.clear(); + }); + + afterEach(() => jest.clearAllMocks()); + + describe('GET /api/upgrade_assistant/reindex/{indexName}', () => { + it('returns the attributes of the reindex operation and reindex warnings', async () => { + mockReindexService.findReindexOperation.mockResolvedValueOnce({ + attributes: { indexName: 'wowIndex', status: ReindexStatus.inProgress }, + }); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.allField]); + + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })(ctxMock, createRequestMock({ params: { indexName: 'wowIndex' } }), kibanaResponseFactory); + + // It called into the service correctly + expect(mockReindexService.findReindexOperation).toHaveBeenCalledWith('wowIndex'); + expect(mockReindexService.detectReindexWarnings).toHaveBeenCalledWith('wowIndex'); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data.reindexOp).toEqual({ indexName: 'wowIndex', status: ReindexStatus.inProgress }); + expect(data.warnings).toEqual([0]); + }); + + it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { + mockReindexService.findReindexOperation.mockResolvedValueOnce(null); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); + + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data.reindexOp).toBeNull(); + expect(data.warnings).toBeNull(); + }); + + it('returns the indexGroup for ML indices', async () => { + mockReindexService.findReindexOperation.mockResolvedValueOnce(null); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); + mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); + + const resp = await serverShim.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data.indexGroup).toEqual(IndexGroup.ml); + }); + }); + + describe('POST /api/upgrade_assistant/reindex/{indexName}', () => { + it('creates a new reindexOp', async () => { + mockReindexService.createReindexOperation.mockResolvedValueOnce({ + attributes: { indexName: 'theIndex' }, + }); + + const resp = await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex'); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ indexName: 'theIndex' }); + }); + + it('calls worker.forceRefresh', async () => { + mockReindexService.createReindexOperation.mockResolvedValueOnce({ + attributes: { indexName: 'theIndex' }, + }); + + await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + + expect(worker.forceRefresh).toHaveBeenCalled(); + }); + + it('inserts headers into the credentialStore', async () => { + const reindexOp = { + attributes: { indexName: 'theIndex' }, + } as ReindexSavedObject; + mockReindexService.createReindexOperation.mockResolvedValueOnce(reindexOp); + + await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })( + ctxMock, + createRequestMock({ + headers: { + 'kbn-auth-x': 'HERE!', + }, + params: { indexName: 'theIndex' }, + }), + kibanaResponseFactory + ); + + expect(credentialStore.get(reindexOp)!['kbn-auth-x']).toEqual('HERE!'); + }); + + it('resumes a reindexOp if it is paused', async () => { + mockReindexService.findReindexOperation.mockResolvedValueOnce({ + attributes: { indexName: 'theIndex', status: ReindexStatus.paused }, + }); + mockReindexService.resumeReindexOperation.mockResolvedValueOnce({ + attributes: { indexName: 'theIndex', status: ReindexStatus.inProgress }, + }); + + const resp = await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })( + ctxMock, + createRequestMock({ + params: { indexName: 'theIndex' }, + }), + kibanaResponseFactory + ); + // It called resume correctly + expect(mockReindexService.resumeReindexOperation).toHaveBeenCalledWith('theIndex'); + expect(mockReindexService.createReindexOperation).not.toHaveBeenCalled(); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ indexName: 'theIndex', status: ReindexStatus.inProgress }); + }); + + it('returns a 403 if required privileges fails', async () => { + mockReindexService.hasRequiredPrivileges.mockResolvedValueOnce(false); + + const resp = await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}', + })( + ctxMock, + createRequestMock({ + params: { indexName: 'theIndex' }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(403); + }); + }); + + describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => { + it('returns a 501', async () => { + mockReindexService.cancelReindexing.mockResolvedValueOnce({}); + + const resp = await serverShim.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/{indexName}/cancel', + })( + ctxMock, + createRequestMock({ + params: { indexName: 'cancelMe' }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ acknowledged: true }); + expect(mockReindexService.cancelReindexing).toHaveBeenCalledWith('cancelMe'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts new file mode 100644 index 0000000000000..c22f12316bd02 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { ReindexStatus } from '../../../common/types'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; +import { CredentialStore } from '../lib/reindexing/credential_store'; +import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; +import { ServerShim, ServerShimWithRouter } from '../types'; +import { createRequestShim } from './create_request_shim'; + +export function registerReindexWorker(server: ServerShim, credentialStore: CredentialStore) { + const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( + 'admin' + ); + const xpackInfo = server.plugins.xpack_main.info; + const savedObjectsRepository = server.savedObjects.getSavedObjectsRepository( + callWithInternalUser + ); + const savedObjectsClient = new server.savedObjects.SavedObjectsClient( + savedObjectsRepository + ) as SavedObjectsClientContract; + + // Cannot pass server.log directly because it's value changes during startup (?). + // Use this function to proxy through. + const log = (tags: string | string[], data?: string | object | (() => any), timestamp?: number) => + server.log(tags, data, timestamp); + + const worker = new ReindexWorker( + savedObjectsClient, + credentialStore, + callWithRequest, + callWithInternalUser, + xpackInfo, + log + ); + + // Wait for ES connection before starting the polling loop. + server.plugins.elasticsearch.waitUntilReady().then(() => { + worker.start(); + server.events.on('stop', () => worker.stop()); + }); + + return worker; +} + +export function registerReindexIndicesRoutes( + server: ServerShimWithRouter, + worker: ReindexWorker, + credentialStore: CredentialStore +) { + const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); + const xpackInfo = server.plugins.xpack_main.info; + const BASE_PATH = '/api/upgrade_assistant/reindex'; + + // Start reindex for an index + server.router.post( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + const { indexName } = reqShim.params; + const { client } = ctx.core.savedObjects; + const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; + const reindexActions = reindexActionsFactory(client, callCluster); + const reindexService = reindexServiceFactory( + callCluster, + xpackInfo, + reindexActions, + server.log + ); + + try { + if (!(await reindexService.hasRequiredPrivileges(indexName))) { + return response.forbidden({ + body: `You do not have adequate privileges to reindex this index.`, + }); + } + + const existingOp = await reindexService.findReindexOperation(indexName); + + // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. + const reindexOp = + existingOp && existingOp.attributes.status === ReindexStatus.paused + ? await reindexService.resumeReindexOperation(indexName) + : await reindexService.createReindexOperation(indexName); + + // Add users credentials for the worker to use + credentialStore.set(reindexOp, reqShim.headers); + + // Kick the worker on this node to immediately pickup the new reindex operation. + worker.forceRefresh(); + + return response.ok({ body: reindexOp.attributes }); + } catch (e) { + return response.internalError({ body: e }); + } + }) + ); + + // Get status + server.router.get( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + const { client } = ctx.core.savedObjects; + const { indexName } = reqShim.params; + const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; + const reindexActions = reindexActionsFactory(client, callCluster); + const reindexService = reindexServiceFactory( + callCluster, + xpackInfo, + reindexActions, + server.log + ); + + try { + const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); + const reindexOp = await reindexService.findReindexOperation(indexName); + // If the user doesn't have privileges than querying for warnings is going to fail. + const warnings = hasRequiredPrivileges + ? await reindexService.detectReindexWarnings(indexName) + : []; + const indexGroup = reindexService.getIndexGroup(indexName); + + return response.ok({ + body: { + reindexOp: reindexOp ? reindexOp.attributes : null, + warnings, + indexGroup, + hasRequiredPrivileges, + }, + }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + }) + ); + + // Cancel reindex + server.router.post( + { + path: `${BASE_PATH}/{indexName}/cancel`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper(async (ctx, request, response) => { + const reqShim = createRequestShim(request); + const { indexName } = reqShim.params; + const { client } = ctx.core.savedObjects; + const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; + const reindexActions = reindexActionsFactory(client, callCluster); + const reindexService = reindexServiceFactory( + callCluster, + xpackInfo, + reindexActions, + server.log + ); + + try { + await reindexService.cancelReindexing(indexName); + + return response.ok({ body: { acknowledged: true } }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts new file mode 100644 index 0000000000000..582c75e3701b6 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { kibanaResponseFactory } from 'src/core/server'; +import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; + +jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ + upsertUIOpenOption: jest.fn(), +})); + +jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ + upsertUIReindexOption: jest.fn(), +})); + +import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; +import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; +import { registerTelemetryRoutes } from './telemetry'; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the lib/telemetry tests. + */ +describe('Upgrade Assistant Telemetry API', () => { + let serverShim: any; + let mockRouter: MockRouter; + let ctxMock: any; + beforeEach(() => { + ctxMock = {}; + mockRouter = createMockRouter(); + serverShim = { + router: mockRouter, + plugins: { + xpack_main: { + info: jest.fn(), + }, + elasticsearch: { + getCluster: () => ({ callWithRequest: jest.fn() } as any), + } as any, + }, + }; + registerTelemetryRoutes(serverShim); + }); + afterEach(() => jest.clearAllMocks()); + + describe('PUT /api/upgrade_assistant/telemetry/ui_open', () => { + it('returns correct payload with single option', async () => { + const returnPayload = { + overview: true, + cluster: false, + indices: false, + }; + + (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + })(ctxMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.payload).toEqual(returnPayload); + }); + + it('returns correct payload with multiple option', async () => { + const returnPayload = { + overview: true, + cluster: true, + indices: true, + }; + + (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + })( + ctxMock, + createRequestMock({ + payload: { + overview: true, + cluster: true, + indices: true, + }, + }), + kibanaResponseFactory + ); + + expect(resp.payload).toEqual(returnPayload); + }); + + it('returns an error if it throws', async () => { + (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + })( + ctxMock, + createRequestMock({ + payload: { + overview: false, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(500); + }); + }); + + describe('PUT /api/upgrade_assistant/telemetry/ui_reindex', () => { + it('returns correct payload with single option', async () => { + const returnPayload = { + close: false, + open: false, + start: true, + stop: false, + }; + + (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + })( + ctxMock, + createRequestMock({ + payload: { + overview: false, + }, + }), + kibanaResponseFactory + ); + + expect(resp.payload).toEqual(returnPayload); + }); + + it('returns correct payload with multiple option', async () => { + const returnPayload = { + close: true, + open: true, + start: true, + stop: true, + }; + + (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + })( + ctxMock, + createRequestMock({ + payload: { + close: true, + open: true, + start: true, + stop: true, + }, + }), + kibanaResponseFactory + ); + + expect(resp.payload).toEqual(returnPayload); + }); + + it('returns an error if it throws', async () => { + (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + + const resp = await serverShim.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + })( + ctxMock, + createRequestMock({ + payload: { + start: false, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(500); + }); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts new file mode 100644 index 0000000000000..f08c49809033d --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; +import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; +import { ServerShimWithRouter } from '../types'; +import { createRequestShim } from './create_request_shim'; + +export function registerTelemetryRoutes(server: ServerShimWithRouter) { + server.router.put( + { + path: '/api/upgrade_assistant/telemetry/ui_open', + validate: { + body: schema.object({ + overview: schema.boolean({ defaultValue: false }), + cluster: schema.boolean({ defaultValue: false }), + indices: schema.boolean({ defaultValue: false }), + }), + }, + }, + async (ctx, request, response) => { + const reqShim = createRequestShim(request); + try { + return response.ok({ body: await upsertUIOpenOption(server, reqShim) }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); + + server.router.put( + { + path: '/api/upgrade_assistant/telemetry/ui_reindex', + validate: { + body: schema.object({ + close: schema.boolean({ defaultValue: false }), + open: schema.boolean({ defaultValue: false }), + start: schema.boolean({ defaultValue: false }), + stop: schema.boolean({ defaultValue: false }), + }), + }, + }, + async (ctx, request, response) => { + const reqShim = createRequestShim(request); + try { + return response.ok({ body: await upsertUIReindexOption(server, reqShim) }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts new file mode 100644 index 0000000000000..f5bcb03f2b6de --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Legacy } from 'kibana'; +import { IRouter } from 'src/core/server'; +import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; + +export interface ServerShim { + plugins: { + elasticsearch: ElasticsearchPlugin; + xpack_main: XPackMainPlugin; + cloud: { + config: { + isCloudEnabled: boolean; + }; + }; + }; + log: any; + events: any; + savedObjects: Legacy.SavedObjectsService; +} + +export interface ServerShimWithRouter extends ServerShim { + router: IRouter; +} + +export interface RequestShim { + headers: Record; + payload: any; + params: any; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts deleted file mode 100644 index 04a05dc2b0e9c..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts +++ /dev/null @@ -1,124 +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 Boom from 'boom'; -import { Server } from 'hapi'; - -jest.mock('../lib/es_version_precheck'); -import { EsVersionPrecheck } from '../lib/es_version_precheck'; -import { registerClusterCheckupRoutes } from './cluster_checkup'; - -// Need to require to get mock on named export to work. -// eslint-disable-next-line @typescript-eslint/no-var-requires -const MigrationApis = require('../lib/es_migration_apis'); -MigrationApis.getUpgradeAssistantStatus = jest.fn(); - -function register(plugins = {}) { - const server = new Server(); - server.plugins = { - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - ...plugins, - } as any; - server.config = () => ({ get: () => '' } as any); - - registerClusterCheckupRoutes(server); - - return server; -} - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the es_migration_apis test. - */ -describe('cluster checkup API', () => { - const spy = jest.spyOn(MigrationApis, 'getUpgradeAssistantStatus'); - - afterEach(() => jest.clearAllMocks()); - - describe('with cloud enabled', () => { - it('is provided to getUpgradeAssistantStatus', async () => { - const server = register({ - cloud: { - config: { - isCloudEnabled: true, - }, - }, - }); - - MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ - cluster: [], - indices: [], - nodes: [], - }); - await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/status', - }); - - expect(spy.mock.calls[0][2]).toBe(true); - }); - }); - - describe('GET /api/upgrade_assistant/reindex/{indexName}.json', () => { - const server = register(); - - it('returns state', async () => { - MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ - cluster: [], - indices: [], - nodes: [], - }); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/status', - }); - - expect(resp.statusCode).toEqual(200); - expect(resp.payload).toMatchInlineSnapshot( - `"{\\"cluster\\":[],\\"indices\\":[],\\"nodes\\":[]}"` - ); - }); - - it('returns an 403 error if it throws forbidden', async () => { - const e: any = new Error(`you can't go here!`); - e.status = 403; - - MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(e); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/status', - }); - - expect(resp.statusCode).toEqual(403); - }); - - it('returns an 500 error if it throws', async () => { - MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/status', - }); - - expect(resp.statusCode).toEqual(500); - }); - - it('returns a 426 if EsVersionCheck throws', async () => { - (EsVersionPrecheck.method as jest.Mock).mockRejectedValue( - new Boom(`blah`, { statusCode: 426 }) - ); - - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/status', - }); - - expect(resp.statusCode).toEqual(426); - }); - }); -}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.ts deleted file mode 100644 index 21c7bc4e5e65d..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/cluster_checkup.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { Legacy } from 'kibana'; -import _ from 'lodash'; - -import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; -import { EsVersionPrecheck } from '../lib/es_version_precheck'; - -export function registerClusterCheckupRoutes(server: Legacy.Server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const isCloudEnabled = _.get(server.plugins, 'cloud.config.isCloudEnabled', false); - - server.route({ - path: '/api/upgrade_assistant/status', - method: 'GET', - options: { - pre: [EsVersionPrecheck], - }, - async handler(request) { - try { - return await getUpgradeAssistantStatus(callWithRequest, request, isCloudEnabled); - } catch (e) { - if (e.status === 403) { - return Boom.forbidden(e.message); - } - - return Boom.boomify(e, { - statusCode: 500, - }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts deleted file mode 100644 index e7918835d461d..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ /dev/null @@ -1,78 +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 { Server } from 'hapi'; - -jest.mock('../lib/es_version_precheck'); -import { registerDeprecationLoggingRoutes } from './deprecation_logging'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the es_deprecation_logging_apis test. - */ -describe('deprecation logging API', () => { - const callWithRequest = jest.fn(); - const server = new Server(); - server.plugins = { - elasticsearch: { - getCluster: () => ({ callWithRequest } as any), - } as any, - } as any; - - registerDeprecationLoggingRoutes(server); - - describe('GET /api/upgrade_assistant/deprecation_logging', () => { - it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'WARN' } } }); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/deprecation_logging', - }); - - expect(resp.statusCode).toEqual(200); - expect(JSON.parse(resp.payload)).toEqual({ isEnabled: true }); - }); - - it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/deprecation_logging', - }); - - expect(resp.statusCode).toEqual(500); - }); - }); - - describe('PUT /api/upgrade_assistant/deprecation_logging', () => { - it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'ERROR' } } }); - const resp = await server.inject({ - method: 'GET', - url: '/api/upgrade_assistant/deprecation_logging', - payload: { - isEnabled: false, - }, - }); - - expect(JSON.parse(resp.payload)).toEqual({ isEnabled: false }); - }); - - it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/deprecation_logging', - payload: { - isEnabled: false, - }, - }); - - expect(resp.statusCode).toEqual(500); - }); - }); -}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.ts deleted file mode 100644 index d16a87916ad7d..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ /dev/null @@ -1,55 +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 Boom from 'boom'; -import Joi from 'joi'; -import { Legacy } from 'kibana'; - -import { - getDeprecationLoggingStatus, - setDeprecationLogging, -} from '../lib/es_deprecation_logging_apis'; -import { EsVersionPrecheck } from '../lib/es_version_precheck'; - -export function registerDeprecationLoggingRoutes(server: Legacy.Server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - - server.route({ - path: '/api/upgrade_assistant/deprecation_logging', - method: 'GET', - options: { - pre: [EsVersionPrecheck], - }, - async handler(request) { - try { - return await getDeprecationLoggingStatus(callWithRequest, request); - } catch (e) { - return Boom.boomify(e, { statusCode: 500 }); - } - }, - }); - - server.route({ - path: '/api/upgrade_assistant/deprecation_logging', - method: 'PUT', - options: { - pre: [EsVersionPrecheck], - validate: { - payload: Joi.object({ - isEnabled: Joi.boolean(), - }), - }, - }, - async handler(request) { - try { - const { isEnabled } = request.payload as { isEnabled: boolean }; - return await setDeprecationLogging(callWithRequest, request, isEnabled); - } catch (e) { - return Boom.boomify(e, { statusCode: 500 }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts deleted file mode 100644 index 264c98526622c..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts +++ /dev/null @@ -1,229 +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 { Server } from 'hapi'; - -const mockReindexService = { - hasRequiredPrivileges: jest.fn(), - detectReindexWarnings: jest.fn(), - getIndexGroup: jest.fn(), - createReindexOperation: jest.fn(), - findAllInProgressOperations: jest.fn(), - findReindexOperation: jest.fn(), - processNextStep: jest.fn(), - resumeReindexOperation: jest.fn(), - cancelReindexing: jest.fn(), -}; - -jest.mock('../lib/es_version_precheck'); -jest.mock('../lib/reindexing', () => { - return { - reindexServiceFactory: () => mockReindexService, - }; -}); - -import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexWarning } from '../../common/types'; -import { credentialStoreFactory } from '../lib/reindexing/credential_store'; -import { registerReindexIndicesRoutes } from './reindex_indices'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the es_migration_apis test. - */ -describe('reindex API', () => { - const server = new Server(); - server.plugins = { - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - xpack_main: { - info: {}, - }, - } as any; - server.config = () => ({ get: () => '' } as any); - server.decorate('request', 'getSavedObjectsClient', () => jest.fn()); - - const credentialStore = credentialStoreFactory(); - - const worker = { - includes: jest.fn(), - forceRefresh: jest.fn(), - } as any; - - registerReindexIndicesRoutes(server, worker, credentialStore); - - beforeEach(() => { - mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); - mockReindexService.detectReindexWarnings.mockReset(); - mockReindexService.getIndexGroup.mockReset(); - mockReindexService.createReindexOperation.mockReset(); - mockReindexService.findAllInProgressOperations.mockReset(); - mockReindexService.findReindexOperation.mockReset(); - mockReindexService.processNextStep.mockReset(); - mockReindexService.resumeReindexOperation.mockReset(); - mockReindexService.cancelReindexing.mockReset(); - worker.includes.mockReset(); - worker.forceRefresh.mockReset(); - - // Reset the credentialMap - credentialStore.clear(); - }); - - describe('GET /api/upgrade_assistant/reindex/{indexName}', () => { - it('returns the attributes of the reindex operation and reindex warnings', async () => { - mockReindexService.findReindexOperation.mockResolvedValueOnce({ - attributes: { indexName: 'wowIndex', status: ReindexStatus.inProgress }, - }); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.allField]); - - const resp = await server.inject({ - method: 'GET', - url: `/api/upgrade_assistant/reindex/wowIndex`, - }); - - // It called into the service correctly - expect(mockReindexService.findReindexOperation).toHaveBeenCalledWith('wowIndex'); - expect(mockReindexService.detectReindexWarnings).toHaveBeenCalledWith('wowIndex'); - - // It returned the right results - expect(resp.statusCode).toEqual(200); - const data = JSON.parse(resp.payload); - expect(data.reindexOp).toEqual({ indexName: 'wowIndex', status: ReindexStatus.inProgress }); - expect(data.warnings).toEqual([0]); - }); - - it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { - mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); - - const resp = await server.inject({ - method: 'GET', - url: `/api/upgrade_assistant/reindex/anIndex`, - }); - - expect(resp.statusCode).toEqual(200); - const data = JSON.parse(resp.payload); - expect(data.reindexOp).toBeNull(); - expect(data.warnings).toBeNull(); - }); - - it('returns the indexGroup for ML indices', async () => { - mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); - mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); - - const resp = await server.inject({ - method: 'GET', - url: `/api/upgrade_assistant/reindex/.ml-state`, - }); - - expect(resp.statusCode).toEqual(200); - const data = JSON.parse(resp.payload); - expect(data.indexGroup).toEqual(IndexGroup.ml); - }); - }); - - describe('POST /api/upgrade_assistant/reindex/{indexName}', () => { - it('creates a new reindexOp', async () => { - mockReindexService.createReindexOperation.mockResolvedValueOnce({ - attributes: { indexName: 'theIndex' }, - }); - - const resp = await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/theIndex', - }); - - // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex'); - - // It returned the right results - expect(resp.statusCode).toEqual(200); - const data = JSON.parse(resp.payload); - expect(data).toEqual({ indexName: 'theIndex' }); - }); - - it('calls worker.forceRefresh', async () => { - mockReindexService.createReindexOperation.mockResolvedValueOnce({ - attributes: { indexName: 'theIndex' }, - }); - - await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/theIndex', - }); - - expect(worker.forceRefresh).toHaveBeenCalled(); - }); - - it('inserts headers into the credentialStore', async () => { - const reindexOp = { - attributes: { indexName: 'theIndex' }, - } as ReindexSavedObject; - mockReindexService.createReindexOperation.mockResolvedValueOnce(reindexOp); - - await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/theIndex', - headers: { - 'kbn-auth-x': 'HERE!', - }, - }); - - expect(credentialStore.get(reindexOp)!['kbn-auth-x']).toEqual('HERE!'); - }); - - it('resumes a reindexOp if it is paused', async () => { - mockReindexService.findReindexOperation.mockResolvedValueOnce({ - attributes: { indexName: 'theIndex', status: ReindexStatus.paused }, - }); - mockReindexService.resumeReindexOperation.mockResolvedValueOnce({ - attributes: { indexName: 'theIndex', status: ReindexStatus.inProgress }, - }); - - const resp = await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/theIndex', - }); - - // It called resume correctly - expect(mockReindexService.resumeReindexOperation).toHaveBeenCalledWith('theIndex'); - expect(mockReindexService.createReindexOperation).not.toHaveBeenCalled(); - - // It returned the right results - expect(resp.statusCode).toEqual(200); - const data = JSON.parse(resp.payload); - expect(data).toEqual({ indexName: 'theIndex', status: ReindexStatus.inProgress }); - }); - - it('returns a 403 if required privileges fails', async () => { - mockReindexService.hasRequiredPrivileges.mockResolvedValueOnce(false); - - const resp = await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/theIndex', - }); - - expect(resp.statusCode).toEqual(403); - }); - }); - - describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => { - it('returns a 501', async () => { - mockReindexService.cancelReindexing.mockResolvedValueOnce({}); - - const resp = await server.inject({ - method: 'POST', - url: '/api/upgrade_assistant/reindex/cancelMe/cancel', - }); - - expect(resp.statusCode).toEqual(200); - expect(resp.payload).toMatchInlineSnapshot(`"{\\"acknowledged\\":true}"`); - expect(mockReindexService.cancelReindexing).toHaveBeenCalledWith('cancelMe'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.ts deleted file mode 100644 index 43e4c9899d233..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/reindex_indices.ts +++ /dev/null @@ -1,190 +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 Boom from 'boom'; -import { Server } from 'hapi'; - -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { ReindexStatus } from '../../common/types'; -import { EsVersionPrecheck } from '../lib/es_version_precheck'; -import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; -import { CredentialStore } from '../lib/reindexing/credential_store'; -import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; - -export function registerReindexWorker(server: Server, credentialStore: CredentialStore) { - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - const xpackInfo = server.plugins.xpack_main.info; - const savedObjectsRepository = server.savedObjects.getSavedObjectsRepository( - callWithInternalUser - ); - const savedObjectsClient = new server.savedObjects.SavedObjectsClient( - savedObjectsRepository - ) as SavedObjectsClientContract; - - // Cannot pass server.log directly because it's value changes during startup (?). - // Use this function to proxy through. - const log: Server['log'] = ( - tags: string | string[], - data?: string | object | (() => any), - timestamp?: number - ) => server.log(tags, data, timestamp); - - const worker = new ReindexWorker( - savedObjectsClient, - credentialStore, - callWithRequest, - callWithInternalUser, - xpackInfo, - log - ); - - // Wait for ES connection before starting the polling loop. - server.plugins.elasticsearch.waitUntilReady().then(() => { - worker.start(); - server.events.on('stop', () => worker.stop()); - }); - - return worker; -} - -export function registerReindexIndicesRoutes( - server: Server, - worker: ReindexWorker, - credentialStore: CredentialStore -) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const xpackInfo = server.plugins.xpack_main.info; - const BASE_PATH = '/api/upgrade_assistant/reindex'; - - // Start reindex for an index - server.route({ - path: `${BASE_PATH}/{indexName}`, - method: 'POST', - options: { - pre: [EsVersionPrecheck], - }, - async handler(request) { - const client = request.getSavedObjectsClient(); - const { indexName } = request.params; - const callCluster = callWithRequest.bind(null, request) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - if (!(await reindexService.hasRequiredPrivileges(indexName))) { - throw Boom.forbidden(`You do not have adequate privileges to reindex this index.`); - } - - const existingOp = await reindexService.findReindexOperation(indexName); - - // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. - const reindexOp = - existingOp && existingOp.attributes.status === ReindexStatus.paused - ? await reindexService.resumeReindexOperation(indexName) - : await reindexService.createReindexOperation(indexName); - - // Add users credentials for the worker to use - credentialStore.set(reindexOp, request.headers); - - // Kick the worker on this node to immediately pickup the new reindex operation. - worker.forceRefresh(); - - return reindexOp.attributes; - } catch (e) { - if (!e.isBoom) { - return Boom.boomify(e, { statusCode: 500 }); - } - - return e; - } - }, - }); - - // Get status - server.route({ - path: `${BASE_PATH}/{indexName}`, - method: 'GET', - options: { - pre: [EsVersionPrecheck], - }, - async handler(request) { - const client = request.getSavedObjectsClient(); - const { indexName } = request.params; - const callCluster = callWithRequest.bind(null, request) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); - const reindexOp = await reindexService.findReindexOperation(indexName); - // If the user doesn't have privileges than querying for warnings is going to fail. - const warnings = hasRequiredPrivileges - ? await reindexService.detectReindexWarnings(indexName) - : []; - const indexGroup = reindexService.getIndexGroup(indexName); - - return { - reindexOp: reindexOp ? reindexOp.attributes : null, - warnings, - indexGroup, - hasRequiredPrivileges, - }; - } catch (e) { - if (!e.isBoom) { - return Boom.boomify(e, { statusCode: 500 }); - } - - return e; - } - }, - }); - - // Cancel reindex - server.route({ - path: `${BASE_PATH}/{indexName}/cancel`, - method: 'POST', - options: { - pre: [EsVersionPrecheck], - }, - async handler(request) { - const client = request.getSavedObjectsClient(); - const { indexName } = request.params; - const callCluster = callWithRequest.bind(null, request) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - await reindexService.cancelReindexing(indexName); - - return { acknowledged: true }; - } catch (e) { - if (!e.isBoom) { - return Boom.boomify(e, { statusCode: 500 }); - } - - return e; - } - }, - }); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.test.js b/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.test.js deleted file mode 100644 index a3706231f2297..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.test.js +++ /dev/null @@ -1,146 +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. - */ - -jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ - upsertUIOpenOption: jest.fn(), -})); - -jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ - upsertUIReindexOption: jest.fn(), -})); - -import { Server } from 'hapi'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { registerTelemetryRoutes } from './telemetry'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry API', () => { - const server = new Server(); - - registerTelemetryRoutes(server); - - describe('PUT /api/upgrade_assistant/telemetry/ui_open', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - overview: true, - cluster: false, - indices: false, - }; - - upsertUIOpenOption.mockResolvedValue(returnPayload); - - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_open', - payload: { - overview: true, - }, - }); - - expect(JSON.parse(resp.payload)).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - overview: true, - cluster: true, - indices: true, - }; - - upsertUIOpenOption.mockResolvedValue(returnPayload); - - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_open', - payload: { - overview: true, - cluster: true, - indices: true, - }, - }); - - expect(JSON.parse(resp.payload)).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - upsertUIOpenOption.mockRejectedValue(new Error(`scary error!`)); - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_open', - payload: { - overview: false, - }, - }); - - expect(resp.statusCode).toEqual(500); - }); - }); - - describe('PUT /api/upgrade_assistant/telemetry/ui_reindex', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - close: false, - open: false, - start: true, - stop: false, - }; - - upsertUIReindexOption.mockResolvedValue(returnPayload); - - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_reindex', - payload: { - start: true, - }, - }); - - expect(JSON.parse(resp.payload)).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - close: true, - open: true, - start: true, - stop: true, - }; - - upsertUIReindexOption.mockResolvedValue(returnPayload); - - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_reindex', - payload: { - close: true, - open: true, - start: true, - stop: true, - }, - }); - - expect(JSON.parse(resp.payload)).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - upsertUIReindexOption.mockRejectedValue(new Error(`scary error!`)); - const resp = await server.inject({ - method: 'PUT', - url: '/api/upgrade_assistant/telemetry/ui_reindex', - payload: { - start: false, - }, - }); - - expect(resp.statusCode).toEqual(500); - }); - }); -}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.ts deleted file mode 100644 index 6def6d1e72ea3..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/routes/telemetry.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import Joi from 'joi'; -import { UpgradeAssistantTelemetryServer } from '../../common/types'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; - -export function registerTelemetryRoutes(server: UpgradeAssistantTelemetryServer) { - server.route({ - path: '/api/upgrade_assistant/telemetry/ui_open', - method: 'PUT', - options: { - validate: { - payload: Joi.object({ - overview: Joi.boolean().default(false), - cluster: Joi.boolean().default(false), - indices: Joi.boolean().default(false), - }), - }, - }, - async handler(request) { - try { - return await upsertUIOpenOption(server, request); - } catch (e) { - return Boom.boomify(e, { statusCode: 500 }); - } - }, - }); - - server.route({ - path: '/api/upgrade_assistant/telemetry/ui_reindex', - method: 'PUT', - options: { - validate: { - payload: Joi.object({ - close: Joi.boolean().default(false), - open: Joi.boolean().default(false), - start: Joi.boolean().default(false), - stop: Joi.boolean().default(false), - }), - }, - }, - async handler(request) { - try { - return await upsertUIReindexOption(server, request); - } catch (e) { - return Boom.boomify(e, { statusCode: 500 }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts index 66ac571e2b7a5..e991e0c6b82e1 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts @@ -18,11 +18,6 @@ export const CLIENT_DEFAULTS = { { start: 'now/M', end: 'now', label: 'Month to date' }, { start: 'now/y', end: 'now', label: 'Year to date' }, ], - /** - * Designate how many checks a monitor summary can have - * before condensing them. - */ - CONDENSED_CHECK_LIMIT: 12, DATE_RANGE_START: 'now-15m', DATE_RANGE_END: 'now', FILTERS: '', diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts index 3a42df8c5e9ab..4c32769d73e84 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts @@ -35,4 +35,10 @@ export const CONTEXT_DEFAULTS = { cursorDirection: CursorDirection.AFTER, sortOrder: SortOrder.ASC, }, + + /** + * Defines the maximum number of monitors to iterate on + * in a single count session. The intention is to catch as many as possible. + */ + MAX_MONITORS_FOR_SNAPSHOT_COUNT: 1000000, }; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts new file mode 100644 index 0000000000000..a88e28f2e5a09 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './snapshot'; +export * from './monitor/monitor_details'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts new file mode 100644 index 0000000000000..246b9c22a08d7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; + +// IO type for validation +export const ErrorType = t.partial({ + code: t.number, + message: t.string, + type: t.string, +}); + +// Typescript type for type checking +export type Error = t.TypeOf; + +export const MonitorDetailsType = t.intersection([ + t.type({ monitorId: t.string }), + t.partial({ error: ErrorType }), +]); +export type MonitorDetails = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts new file mode 100644 index 0000000000000..99bf783d3ab2e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Snapshot, SnapshotType } from './snapshot_count'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts new file mode 100644 index 0000000000000..d4935c50ff5b8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const SnapshotType = t.type({ + down: t.number, + mixed: t.number, + total: t.number, + up: t.number, +}); + +export type Snapshot = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index c2fb0a2ad6bd7..690807cc91e27 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -37,6 +37,8 @@ export const uptime = (kibana: any) => const initializerContext = {} as PluginInitializerContext; const { savedObjects } = server; const { elasticsearch, xpack_main } = server.plugins; + const { usageCollection } = server.newPlatform.setup.plugins; + plugin(initializerContext).setup( { route: (arg: any) => server.route(arg), @@ -44,7 +46,7 @@ export const uptime = (kibana: any) => { elasticsearch, savedObjects, - usageCollector: server.usage, + usageCollection, xpack: xpack_main, } ); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts index 3b328b3ff2326..53a74022778f4 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/index.ts @@ -8,4 +8,7 @@ import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; -new Plugin({ opaqueId: Symbol('uptime'), env: {} as any }, chrome).start(npStart); +new Plugin( + { opaqueId: Symbol('uptime'), env: {} as any, config: { get: () => ({} as any) } }, + chrome +).start(npStart); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap index 56bc101f59dfa..45c24fd11194d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_ssl_certificate.test.tsx.snap @@ -5,8 +5,12 @@ Array [

, -
{ - const snapshot: SnapshotType = { - counts: { - up: 8, - down: 2, - mixed: 0, - total: 10, - }, + const snapshot: Snapshot = { + up: 8, + down: 2, + mixed: 0, + total: 10, }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap new file mode 100644 index 0000000000000..3f3e6b0b929e1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChartWrapper component renders the component with loading false 1`] = ` + +
+ + + +
+
+`; + +exports[`ChartWrapper component renders the component with loading true 1`] = ` + +
+ + + +
+ + + + + +
+`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index 82d21c3f2d5ad..0d1ced78f4a49 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -24,6 +24,27 @@ exports[`DonutChart component passes correct props without errors for valid prop `; exports[`DonutChart component renders a donut chart 1`] = ` +.c1.c1 { + margin-left: 0px; + margin-right: 0px; +} + +.c2 { + text-align: right; +} + +.c0 { + max-width: 260px; + min-width: 100px; +} + +@media (max-width:767px) { + .c0 { + min-width: 0px; + max-width: 100px; + } +} +
@@ -40,13 +61,13 @@ exports[`DonutChart component renders a donut chart 1`] = ` class="euiFlexItem" >
Down 32 @@ -91,7 +112,7 @@ exports[`DonutChart component renders a donut chart 1`] = ` class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow" >
Up 95 diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx new file mode 100644 index 0000000000000..43e6b80d5c840 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { mount } from 'enzyme'; +import { nextTick } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { ChartWrapper } from '../chart_wrapper'; +import { SnapshotHeading } from '../../snapshot_heading'; +import { DonutChart } from '../donut_chart'; +const SNAPSHOT_CHART_WIDTH = 144; +const SNAPSHOT_CHART_HEIGHT = 144; +describe('ChartWrapper component', () => { + it('renders the component with loading false', () => { + const component = shallowWithIntl( + + + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders the component with loading true', () => { + const component = shallowWithIntl( + + + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('mounts the component with loading true or false', async () => { + const component = mount( + + + + + + ); + + let loadingChart = component.find(`.euiLoadingChart`); + expect(loadingChart.length).toBe(1); + + component.setProps({ + loading: false, + }); + await nextTick(); + component.update(); + + loadingChart = component.find(`.euiLoadingChart`); + expect(loadingChart.length).toBe(0); + }); + + it('mounts the component with chart when loading true or false', async () => { + const component = mount( + + + + + + ); + + let donutChart = component.find(DonutChart); + expect(donutChart.length).toBe(1); + + component.setProps({ + loading: false, + }); + await nextTick(); + component.update(); + + donutChart = component.find(DonutChart); + expect(donutChart.length).toBe(1); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx index 4f12af75815e8..6e73090782b04 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx @@ -9,7 +9,7 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { UptimeFilterButton } from './uptime_filter_button'; import { toggleSelectedItems } from './toggle_selected_item'; -import { LocationLink } from '../monitor_list/location_link'; +import { LocationLink } from '../monitor_list'; export interface FilterPopoverProps { fieldName: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts index bb8a1a3a2474a..42316fca99f34 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts @@ -12,7 +12,6 @@ export { IntegrationLink } from './integration_link'; export { KueryBar } from './kuery_bar'; export { MonitorCharts } from './monitor_charts'; export { MonitorList } from './monitor_list'; -export { MonitorPageLink } from './monitor_page_link'; export { MonitorPageTitle } from './monitor_page_title'; export { MonitorStatusBar } from './monitor_status_bar'; export { OverviewPageParsingErrorCallout } from './overview_page_parsing_error_callout'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index f529c9cd9d53f..da392660eb70e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -9,14 +9,17 @@ import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public'; -import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns'; import { Typeahead } from './typeahead'; import { getIndexPattern } from '../../../lib/adapters/index_pattern'; import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; +import { + AutocompleteProviderRegister, + AutocompleteSuggestion, + esKuery, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; @@ -27,15 +30,15 @@ interface State { isLoadingIndexPattern: boolean; } -function convertKueryToEsQuery(kuery: string, indexPattern: unknown) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( query: string, selectionStart: number, - apmIndexPattern: StaticIndexPattern, + apmIndexPattern: IIndexPattern, autocomplete: Pick ) { const autocompleteProvider = autocomplete.getProvider('kuery'); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/check_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/check_list.test.tsx.snap deleted file mode 100644 index 7aa77c7daf60f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/check_list.test.tsx.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CheckList component renders a list of checks 1`] = ` - - - - - - - - - - 127.0.0.1 - - - - - - - - - - - 127.0.0.2 - - - -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/condensed_check_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/condensed_check_list.test.tsx.snap deleted file mode 100644 index 58b0887c29b32..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/condensed_check_list.test.tsx.snap +++ /dev/null @@ -1,253 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CondensedCheckList component renders checks 1`] = ` - - - - - - - - - - - - - - - - - - 127.0.0.1 - - - a few moments ago - - , - - - - - - 127.0.0.2 - - - a few moments ago - - , - ] - } - delay="regular" - position="right" - title="Check statuses" - > - - 2 checks - - - - - - - - - - - - - - - - - - - - 127.0.0.1 - - - a few moments ago - - , - - - - - - 127.0.0.2 - - - a few moments ago - - , - ] - } - delay="regular" - position="right" - title="Check statuses" - > - - 2 checks - - - - -`; - -exports[`CondensedCheckList component renders null in place of child status with missing ip 1`] = ` - - - - - - - - - - - - - - - - - - 127.0.0.2 - - - a few moments ago - - , - ] - } - delay="regular" - position="right" - title="Check statuses" - > - - 2 checks - - - - - - - - - - - - - - - - - - - - 127.0.0.1 - - - a few moments ago - - , - - - - - - 127.0.0.2 - - - a few moments ago - - , - ] - } - delay="regular" - position="right" - title="Check statuses" - > - - 2 checks - - - - -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap deleted file mode 100644 index 3b0f76dfb1088..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorListDrawer component renders a Checklist when there is only one check 1`] = ` - -`; - -exports[`MonitorListDrawer component renders a CondensedCheckList when there are many checks 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap new file mode 100644 index 0000000000000..e52977749142d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorPageLink component renders a help link when link parameters present 1`] = ` + + + +`; + +exports[`MonitorPageLink component renders the link properly 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/to_condensed_check.test.ts.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/to_condensed_check.test.ts.snap deleted file mode 100644 index 44c284d551841..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/to_condensed_check.test.ts.snap +++ /dev/null @@ -1,104 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`toCondensedCheck condenses checks across location 1`] = ` -Array [ - Object { - "childStatuses": Array [ - Object { - "ip": "192.178.123.21", - "status": "up", - "timestamp": "123", - }, - Object { - "ip": "192.178.123.22", - "status": "down", - "timestamp": "124", - }, - Object { - "ip": "192.178.123.23", - "status": "up", - "timestamp": "113", - }, - ], - "location": "us-east-1", - "status": "mixed", - "timestamp": "124", - }, -] -`; - -exports[`toCondensedCheck creates the correct number of condensed checks for multiple locations 1`] = ` -Array [ - Object { - "childStatuses": Array [ - Object { - "ip": "192.178.123.21", - "status": "up", - "timestamp": "123", - }, - Object { - "ip": "192.178.123.22", - "status": "down", - "timestamp": "124", - }, - Object { - "ip": "192.178.123.23", - "status": "up", - "timestamp": "113", - }, - ], - "location": "us-east-1", - "status": "mixed", - "timestamp": "124", - }, - Object { - "childStatuses": Array [ - Object { - "ip": "192.178.123.21", - "status": "up", - "timestamp": "121", - }, - Object { - "ip": "192.178.123.22", - "status": "down", - "timestamp": "132", - }, - Object { - "ip": "192.178.123.23", - "status": "up", - "timestamp": "115", - }, - ], - "location": "us-west-1", - "status": "mixed", - "timestamp": "132", - }, -] -`; - -exports[`toCondensedCheck infers an "up" status for a series of "up" checks 1`] = ` -Array [ - Object { - "childStatuses": Array [ - Object { - "ip": "192.178.123.21", - "status": "up", - "timestamp": "123", - }, - Object { - "ip": "192.178.123.22", - "status": "up", - "timestamp": "124", - }, - Object { - "ip": "192.178.123.23", - "status": "up", - "timestamp": "113", - }, - ], - "location": "us-east-1", - "status": "up", - "timestamp": "124", - }, -] -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/check_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/check_list.test.tsx deleted file mode 100644 index bc0e1f11002fd..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/check_list.test.tsx +++ /dev/null @@ -1,49 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import moment from 'moment'; -import React from 'react'; -import { Check } from '../../../../../common/graphql/types'; -import { CheckList } from '../check_list'; - -describe('CheckList component', () => { - let checks: Check[]; - - beforeAll(() => { - moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); - moment.prototype.from = jest.fn(() => 'a few moments ago'); - }); - - beforeEach(() => { - checks = [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '123', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'up', - }, - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: 'up', - }, - ]; - }); - - it('renders a list of checks', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/condensed_check_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/condensed_check_list.test.tsx deleted file mode 100644 index 378167b6e5b05..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/condensed_check_list.test.tsx +++ /dev/null @@ -1,74 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import moment from 'moment'; -import { CondensedCheck } from '../types'; -import { CondensedCheckList } from '../condensed_check_list'; - -describe('CondensedCheckList component', () => { - let checks: CondensedCheck[]; - - beforeAll(() => { - moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); - moment.prototype.from = jest.fn(() => 'a few moments ago'); - }); - - beforeEach(() => { - checks = [ - { - childStatuses: [ - { - ip: '127.0.0.1', - status: 'up', - timestamp: '123', - }, - { - ip: '127.0.0.2', - status: 'down', - timestamp: '122', - }, - ], - location: 'us-east-1', - status: 'mixed', - timestamp: '123', - }, - { - childStatuses: [ - { - ip: '127.0.0.1', - status: 'up', - timestamp: '120', - }, - { - ip: '127.0.0.2', - status: 'up', - timestamp: '121', - }, - ], - location: 'us-west-1', - status: 'up', - timestamp: '125', - }, - ]; - }); - - it('renders checks', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); - - it('renders null in place of child status with missing ip', () => { - checks[0].childStatuses[0].ip = undefined; - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_drawer.test.tsx deleted file mode 100644 index cd3511257bcaa..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_drawer.test.tsx +++ /dev/null @@ -1,104 +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 { MonitorSummary, Check } from '../../../../../common/graphql/types'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { MonitorListDrawer } from '../monitor_list_drawer'; - -describe('MonitorListDrawer component', () => { - let summary: MonitorSummary; - - beforeEach(() => { - summary = { - monitor_id: 'foo', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '121', - }, - ], - summary: { - up: 1, - down: 0, - }, - timestamp: '123', - }, - }; - }); - - it('renders nothing when no summary data is present', () => { - const component = shallowWithIntl( - - ); - expect(component).toEqual({}); - }); - - it('renders nothing when no check data is present', () => { - delete summary.state.checks; - const component = shallowWithIntl( - - ); - expect(component).toEqual({}); - }); - - it('renders a Checklist when there is only one check', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); - - it('renders a CondensedCheckList when there are many checks', () => { - const checks: Check[] = [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '121', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: '123', - }, - { - monitor: { - ip: '127.0.0.3', - status: 'up', - }, - timestamp: '125', - }, - ]; - summary.state.checks = checks; - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_page_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_page_link.test.tsx new file mode 100644 index 0000000000000..dd6e9c66d395b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_page_link.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { MonitorPageLink } from '../monitor_page_link'; + +describe('MonitorPageLink component', () => { + it('renders the link properly', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders a help link when link parameters present', () => { + const linkParameters = 'selectedPingStatus=down'; + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/to_condensed_check.test.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/to_condensed_check.test.ts deleted file mode 100644 index 6b8a2fd3c0e78..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/to_condensed_check.test.ts +++ /dev/null @@ -1,178 +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 { Check } from '../../../../../common/graphql/types'; -import { toCondensedCheck } from '../to_condensed_check'; - -describe('toCondensedCheck', () => { - let checks: Check[]; - beforeEach(() => { - checks = [ - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '123', - monitor: { - ip: '192.178.123.21', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '124', - monitor: { - ip: '192.178.123.22', - status: 'down', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '113', - monitor: { - ip: '192.178.123.23', - status: 'up', - }, - }, - ]; - }); - - it('condenses checks across location', () => { - expect(toCondensedCheck(checks)).toMatchSnapshot(); - }); - - it('infers an "up" status for a series of "up" checks', () => { - checks = [ - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '123', - monitor: { - ip: '192.178.123.21', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '124', - monitor: { - ip: '192.178.123.22', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '113', - monitor: { - ip: '192.178.123.23', - status: 'up', - }, - }, - ]; - const result = toCondensedCheck(checks); - expect(result).toMatchSnapshot(); - }); - - it('creates the correct number of condensed checks for multiple locations', () => { - checks = [ - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '123', - monitor: { - ip: '192.178.123.21', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '124', - monitor: { - ip: '192.178.123.22', - status: 'down', - }, - }, - { - observer: { - geo: { - name: 'us-east-1', - }, - }, - timestamp: '113', - monitor: { - ip: '192.178.123.23', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-west-1', - }, - }, - timestamp: '121', - monitor: { - ip: '192.178.123.21', - status: 'up', - }, - }, - { - observer: { - geo: { - name: 'us-west-1', - }, - }, - timestamp: '132', - monitor: { - ip: '192.178.123.22', - status: 'down', - }, - }, - { - observer: { - geo: { - name: 'us-west-1', - }, - }, - timestamp: '115', - monitor: { - ip: '192.178.123.23', - status: 'up', - }, - }, - ]; - const result = toCondensedCheck(checks); - expect(result).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/check_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/check_list.tsx deleted file mode 100644 index 8beb3f7fa5a37..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/check_list.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiText } from '@elastic/eui'; -import { get } from 'lodash'; -import { MonitorListStatusColumn } from './monitor_list_status_column'; -import { Check } from '../../../../common/graphql/types'; -import { LocationLink } from './location_link'; - -interface CheckListProps { - checks: Check[]; -} - -export const CheckList = ({ checks }: CheckListProps) => { - return ( - - {checks.map(check => { - const location = get(check, 'observer.geo.name', null); - const agentId = get(check, 'agent.id', 'null'); - const key = location + agentId + check.monitor.ip; - return ( - - - - - - - - - - {check.monitor.ip} - - - - ); - })} - - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/condensed_check_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/condensed_check_list.tsx deleted file mode 100644 index 7ac4b6e82602f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/condensed_check_list.tsx +++ /dev/null @@ -1,94 +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 { - EuiFlexGrid, - EuiFlexItem, - EuiHealth, - EuiToolTip, - EuiBadge, - EuiFlexGroup, -} from '@elastic/eui'; -import moment from 'moment'; -import React from 'react'; -import { CondensedCheck, CondensedCheckStatus } from './types'; -import { MonitorListStatusColumn } from './monitor_list_status_column'; -import { LocationLink } from './location_link'; - -const getBadgeColor = (status: string, successColor: string, dangerColor: string) => { - switch (status) { - case 'up': - return successColor; - case 'down': - return dangerColor; - case 'mixed': - return 'secondary'; - default: - return undefined; - } -}; - -const getHealthColor = (dangerColor: string, status: string, successColor: string) => { - switch (status) { - case 'up': - return successColor; - case 'down': - return dangerColor; - default: - return 'primary'; - } -}; - -interface CondensedCheckListProps { - condensedChecks: CondensedCheck[]; - successColor: string; - dangerColor: string; -} - -export const CondensedCheckList = ({ - condensedChecks, - dangerColor, - successColor, -}: CondensedCheckListProps) => ( - - {condensedChecks.map(({ childStatuses, location, status, timestamp }: CondensedCheck) => ( - - - - - - - - - - - - - - ip ? ( - - - - - {ip} - {moment(parseInt(condensedTimestamp, 10)).fromNow()} - - ) : null - )} - > - {`${childStatuses.length} checks`} - - - - ))} - -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts index 717e66e4dbe1a..a83330a7a3a0b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts @@ -6,3 +6,4 @@ export { MonitorList } from './monitor_list'; export { Criteria, Pagination } from './types'; +export { LocationLink } from './monitor_list_drawer'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index 5774453ff67ab..274a7ab0be9be 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -30,9 +30,8 @@ import { MonitorListStatusColumn } from './monitor_list_status_column'; import { formatUptimeGraphQLErrorList } from '../../../lib/helper/format_error_list'; import { ExpandedRowMap } from './types'; import { MonitorListDrawer } from './monitor_list_drawer'; -import { CLIENT_DEFAULTS } from '../../../../common/constants'; import { MonitorBarSeries } from '../charts'; -import { MonitorPageLink } from '../monitor_page_link'; +import { MonitorPageLink } from './monitor_page_link'; import { MonitorListActionsPopover } from './monitor_list_actions_popover'; import { OverviewPageLink } from '../overview_page_link'; @@ -56,7 +55,6 @@ export const MonitorListComponent = (props: Props) => { absoluteStartDate, absoluteEndDate, dangerColor, - successColor, data, errors, hasActiveFilters, @@ -69,6 +67,19 @@ export const MonitorListComponent = (props: Props) => { const nextPagePagination = get(data, 'monitorStates.nextPagePagination'); const prevPagePagination = get(data, 'monitorStates.prevPagePagination'); + const getExpandedRowMap = () => { + return drawerIds.reduce((map: ExpandedRowMap, id: string) => { + return { + ...map, + [id]: ( + monitorId === id)} + /> + ), + }; + }, {}); + }; + return ( @@ -95,21 +106,7 @@ export const MonitorListComponent = (props: Props) => { isExpandable={true} hasActions={true} itemId="monitor_id" - itemIdToExpandedRowMap={drawerIds.reduce((map: ExpandedRowMap, id: string) => { - return { - ...map, - [id]: ( - monitorId === id) : undefined - } - successColor={successColor} - dangerColor={dangerColor} - /> - ), - }; - }, {})} + itemIdToExpandedRowMap={getExpandedRowMap()} items={items} // TODO: not needed without sorting and pagination // onChange={onChange} @@ -148,11 +145,7 @@ export const MonitorListComponent = (props: Props) => { defaultMessage: 'Name', }), render: (name: string, summary: MonitorSummary) => ( - + {name ? name : `Unnamed - ${summary.monitor_id}`} ), @@ -231,9 +224,9 @@ export const MonitorListComponent = (props: Props) => { }, } )} - iconType={drawerIds.find(item => item === id) ? 'arrowUp' : 'arrowDown'} + iconType={drawerIds.includes(id) ? 'arrowUp' : 'arrowDown'} onClick={() => { - if (drawerIds.find(i => id === i)) { + if (drawerIds.includes(id)) { updateDrawerIds(drawerIds.filter(p => p !== id)); } else { updateDrawerIds([...drawerIds, id]); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer.tsx deleted file mode 100644 index ac54518b6f49f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer.tsx +++ /dev/null @@ -1,47 +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 React from 'react'; -import { MonitorSummary } from '../../../../common/graphql/types'; -import { CheckList } from './check_list'; -import { toCondensedCheck } from './to_condensed_check'; -import { CondensedCheckList } from './condensed_check_list'; - -interface MonitorListDrawerProps { - summary?: MonitorSummary; - dangerColor: string; - successColor: string; - /** - * The number of checks the component should fully render - * before squashing them to single rows with condensed details. - */ - condensedCheckLimit: number; -} - -/** - * The elements shown when the user expands the monitor list rows. - */ -export const MonitorListDrawer = ({ - condensedCheckLimit, - dangerColor, - successColor, - summary, -}: MonitorListDrawerProps) => { - if (!summary || !summary.state.checks) { - return null; - } - if (summary.state.checks.length < condensedCheckLimit) { - return ; - } else { - return ( - - ); - } -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/location_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/location_link.test.tsx.snap similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/location_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/location_link.test.tsx.snap index 281372023ce29..877f1fc6d7c85 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/location_link.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/location_link.test.tsx.snap @@ -16,6 +16,7 @@ exports[`LocationLink component renders a help link when location not present 1` exports[`LocationLink component renders the location when present 1`] = ` us-east-1 diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap new file mode 100644 index 0000000000000..80e064e25e1a5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorListDrawer component renders a MonitorListDrawer when there are many checks 1`] = ` + + + + + https://expired.badssl.com + + + + + + + + +`; + +exports[`MonitorListDrawer component renders a MonitorListDrawer when there is only one check 1`] = ` + + + + + https://expired.badssl.com + + + + + + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap new file mode 100644 index 0000000000000..94162a19a2daa --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorStatusList component renders checks 1`] = ` + + + + + , + } + } + /> + + +`; + +exports[`MonitorStatusList component renders null in place of child status with missing ip 1`] = ` + + + + + , + } + } + /> + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap new file mode 100644 index 0000000000000..e2caf6f718728 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorStatusRow component renders status row when status is down 1`] = ` + + + + + + +`; + +exports[`MonitorStatusRow component renders status row when status is up 1`] = ` + + + + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap new file mode 100644 index 0000000000000..cdda21b75770a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MostRecentError component renders properly with empty data 1`] = ` +Array [ +
+

+ Most recent error +

+
, + , +] +`; + +exports[`MostRecentError component validates props with shallow render 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json new file mode 100644 index 0000000000000..64adf3642fb22 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json @@ -0,0 +1,623 @@ +{ + "data": { + "monitorStates": { + "prevPagePagination": null, + "nextPagePagination": null, + "totalSummaryCount": { "count": 147428, "__typename": "DocCount" }, + "summaries": [ + { + "monitor_id": "andrewvc-com", + "histogram": { + "count": 60, + "points": [ + { + "timestamp": 1570538088000, + "up": 8, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 16, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 12, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 16, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 8, + "down": 0, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.108.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.109.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.110.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.111.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + } + ], + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": null, + "status": "up", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 4, "down": 0, "geo": null, "__typename": "Summary" }, + "url": { + "full": "http://blog.andrewvc.com", + "domain": "blog.andrewvc.com", + "__typename": "StateUrl" + }, + "timestamp": 1570538246145, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + }, + { + "monitor_id": "bad-ssl", + "histogram": { + "count": 16, + "points": [ + { + "timestamp": 1570538088000, + "up": 0, + "down": 3, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 0, + "down": 4, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 0, + "down": 3, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 0, + "down": 4, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 0, + "down": 2, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "104.154.89.105", + "name": "", + "status": "down", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246144", + "__typename": "Check" + } + ], + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": null, + "status": "down", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 0, "down": 1, "geo": null, "__typename": "Summary" }, + "url": { + "full": "https://expired.badssl.com", + "domain": "expired.badssl.com", + "__typename": "StateUrl" + }, + "timestamp": 1570538246144, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + }, + { + "monitor_id": "elastic-co", + "histogram": { + "count": 72, + "points": [ + { + "timestamp": 1570538088000, + "up": 4, + "down": 4, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 8, + "down": 8, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 8, + "down": 8, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 12, + "down": 12, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 4, + "down": 4, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": , + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": "elastic", + "status": "mixed", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 4, "down": 4, "geo": null, "__typename": "Summary" }, + "url": { + "full": "https://www.elastic.co", + "domain": "www.elastic.co", + "__typename": "StateUrl" + }, + "timestamp": 1570538236414, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + }, + { + "monitor_id": "kibana-local", + "histogram": { + "count": 66, + "points": [ + { + "timestamp": 1570538052000, + "up": 1, + "down": 1, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538088000, + "up": 7, + "down": 7, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 7, + "down": 7, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 7, + "down": 7, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 8, + "down": 8, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 3, + "down": 3, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "127.0.0.1", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246144", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "::1", + "name": "", + "status": "down", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + } + ], + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": null, + "status": "mixed", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 1, "down": 1, "geo": null, "__typename": "Summary" }, + "url": { + "full": "http://localhost:5601/", + "domain": "localhost", + "__typename": "StateUrl" + }, + "timestamp": 1570538246145, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + }, + { + "monitor_id": "localhost", + "histogram": { + "count": 28, + "points": [ + { + "timestamp": 1570538052000, + "up": 12, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538088000, + "up": 3, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 4, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 3, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 4, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 2, + "down": 0, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "127.0.0.1", + "name": "localhost", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246143", + "__typename": "Check" + } + ], + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": "localhost", + "status": "up", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 1, "down": 0, "geo": null, "__typename": "Summary" }, + "url": { + "full": "http://localhost:9200", + "domain": "localhost", + "__typename": "StateUrl" + }, + "timestamp": 1570538246143, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + }, + { + "monitor_id": "secure-avc", + "histogram": { + "count": 64, + "points": [ + { + "timestamp": 1570538088000, + "up": 12, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538124000, + "up": 16, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538160000, + "up": 12, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538196000, + "up": 16, + "down": 0, + "__typename": "SummaryHistogramPoint" + }, + { + "timestamp": 1570538232000, + "up": 8, + "down": 0, + "__typename": "SummaryHistogramPoint" + } + ], + "__typename": "SummaryHistogram" + }, + "state": { + "agent": null, + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.108.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.109.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.110.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + }, + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "185.199.111.153", + "name": "", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246145", + "__typename": "Check" + } + ], + "geo": null, + "observer": { + "geo": { "name": [], "location": null, "__typename": "StateGeo" }, + "__typename": "StateObserver" + }, + "monitor": { + "id": null, + "name": null, + "status": "up", + "type": null, + "__typename": "MonitorState" + }, + "summary": { "up": 4, "down": 0, "geo": null, "__typename": "Summary" }, + "url": { + "full": "https://blog.andrewvc.com", + "domain": "blog.andrewvc.com", + "__typename": "StateUrl" + }, + "timestamp": 1570538246145, + "error": null, + "__typename": "State" + }, + "__typename": "MonitorSummary" + } + ], + "__typename": "MonitorSummaryResult" + } + } +} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/location_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/location_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/location_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/location_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx new file mode 100644 index 0000000000000..aca43f550aa14 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 'jest'; +import { MonitorSummary, Check } from '../../../../../../common/graphql/types'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { MonitorListDrawerComponent } from '../monitor_list_drawer'; + +describe('MonitorListDrawer component', () => { + let summary: MonitorSummary; + let loadMonitorDetails: any; + let monitorDetails: any; + + beforeEach(() => { + summary = { + monitor_id: 'foo', + state: { + checks: [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: '121', + }, + ], + summary: { + up: 1, + down: 0, + }, + timestamp: '123', + url: { + domain: 'expired.badssl.com', + full: 'https://expired.badssl.com', + }, + }, + }; + monitorDetails = { + monitorId: 'bad-ssl', + error: { + type: 'io', + message: + 'Get https://expired.badssl.com: x509: certificate has expired or is not yet valid', + }, + }; + loadMonitorDetails = () => null; + }); + + it('renders nothing when no summary data is present', () => { + const component = shallowWithIntl( + + ); + expect(component).toEqual({}); + }); + + it('renders nothing when no check data is present', () => { + delete summary.state.checks; + const component = shallowWithIntl( + + ); + expect(component).toEqual({}); + }); + + it('renders a MonitorListDrawer when there is only one check', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders a MonitorListDrawer when there are many checks', () => { + const checks: Check[] = [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: '121', + }, + { + monitor: { + ip: '127.0.0.2', + status: 'down', + }, + timestamp: '123', + }, + { + monitor: { + ip: '127.0.0.3', + status: 'up', + }, + timestamp: '125', + }, + ]; + summary.state.checks = checks; + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx new file mode 100644 index 0000000000000..8c07d0b1a7d22 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import moment from 'moment'; +import { MonitorStatusList } from '../monitor_status_list'; +import { Check } from '../../../../../../common/graphql/types'; + +describe('MonitorStatusList component', () => { + let checks: Check[]; + + beforeAll(() => { + moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); + moment.prototype.from = jest.fn(() => 'a few moments ago'); + }); + + beforeEach(() => { + checks = [ + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + monitor: { + ip: '151.101.130.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + monitor: { + ip: '151.101.194.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + monitor: { + ip: '151.101.2.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + container: null, + kubernetes: null, + monitor: { + ip: '151.101.66.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + container: null, + kubernetes: null, + monitor: { + ip: '2a04:4e42:200::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + container: null, + kubernetes: null, + monitor: { + ip: '2a04:4e42:400::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + container: null, + kubernetes: null, + monitor: { + ip: '2a04:4e42:600::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + { + agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, + container: null, + kubernetes: null, + monitor: { + ip: '2a04:4e42::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: { name: null, location: null }, + }, + timestamp: '1570538236414', + }, + ]; + }); + + it('renders checks', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('renders null in place of child status with missing ip', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx new file mode 100644 index 0000000000000..0353d0197f7f7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { MonitorStatusRow } from '../monitor_status_row'; + +describe('MonitorStatusRow component', () => { + let locationNames: Set; + + beforeEach(() => { + locationNames = new Set(['Berlin', 'Islamabad', 'London']); + }); + + it('renders status row when status is up', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders status row when status is down', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx new file mode 100644 index 0000000000000..71eab73cd52d6 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { MostRecentError } from '../most_recent_error'; + +describe('MostRecentError component', () => { + let monitorDetails: any; + + beforeEach(() => { + monitorDetails = { + monitorId: 'bad-ssl', + error: { + type: 'io', + message: + 'Get https://expired.badssl.com: x509: certificate has expired or is not yet valid', + }, + }; + }); + + it('validates props with shallow render', () => { + const component = shallowWithIntl( + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders properly with empty data', () => { + const component = renderWithIntl( + + + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts new file mode 100644 index 0000000000000..73fb07db60de8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MonitorListDrawer } from './monitor_list_drawer'; +export { LocationLink } from './location_link'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/location_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/location_link.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/location_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/location_link.tsx index 70aaebc4d358e..72c1dbfd85604 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/location_link.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/location_link.tsx @@ -22,7 +22,9 @@ const locationDocsLink = */ export const LocationLink = ({ location, textSize }: LocationLinkProps) => { return location ? ( - {location} + + {location} + ) : ( {i18n.translate('xpack.uptime.monitorList.geoName.helpLinkAnnotation', { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx new file mode 100644 index 0000000000000..25f8f9718d411 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { get } from 'lodash'; +import styled from 'styled-components'; +import { connect } from 'react-redux'; +import { MonitorSummary } from '../../../../../common/graphql/types'; +import { AppState } from '../../../../state'; +import { fetchMonitorDetails } from '../../../../state/actions/monitor'; +import { MostRecentError } from './most_recent_error'; +import { getMonitorDetails } from '../../../../state/selectors'; +import { MonitorStatusList } from './monitor_status_list'; + +const ContainerDiv = styled.div` + padding: 10px; + width: 100%; +`; + +interface MonitorListDrawerProps { + /** + * Monitor Summary + */ + summary: MonitorSummary; + + /** + * Monitor details to be fetched from rest api using monitorId + */ + monitorDetails: any; + + /** + * Redux action to trigger , loading monitor details + */ + loadMonitorDetails: typeof fetchMonitorDetails; +} + +/** + * The elements shown when the user expands the monitor list rows. + */ + +export function MonitorListDrawerComponent({ + summary, + loadMonitorDetails, + monitorDetails, +}: MonitorListDrawerProps) { + if (!summary || !summary.state.checks) { + return null; + } + useEffect(() => { + loadMonitorDetails(summary.monitor_id); + }, []); + + const monitorUrl: string | undefined = get(summary.state.url, 'full', undefined); + + return ( + + + + + {monitorUrl} + + + + + + + {monitorDetails && monitorDetails.error && ( + + )} + + ); +} + +const mapStateToProps = (state: AppState, { summary }: any) => ({ + monitorDetails: getMonitorDetails(state, summary), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + loadMonitorDetails: (monitorId: string) => dispatch(fetchMonitorDetails(monitorId)), +}); + +export const MonitorListDrawer = connect( + mapStateToProps, + mapDispatchToProps +)(MonitorListDrawerComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx new file mode 100644 index 0000000000000..82e415cd5e8ae --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { get } from 'lodash'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Check } from '../../../../../common/graphql/types'; +import { LocationLink } from './location_link'; +import { MonitorStatusRow } from './monitor_status_row'; + +interface MonitorStatusListProps { + /** + * Recent List of checks performed on monitor + */ + checks: Check[]; +} + +export const UP = 'up'; +export const DOWN = 'down'; +export const UNNAMED_LOCATION = 'unnamed-location'; + +export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { + const upChecks: Set = new Set(); + const downChecks: Set = new Set(); + + checks.forEach((check: Check) => { + // Doing this way because name is either string or null, get() default value only works on undefined value + const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; + + if (check.monitor.status === UP) { + upChecks.add(location); + } else if (check.monitor.status === DOWN) { + downChecks.add(location); + } + }); + + // if monitor is down in one dns, it will be considered down so removing it from up list + const absUpChecks: Set = new Set([...upChecks].filter(item => !downChecks.has(item))); + + return ( + <> + + + {(downChecks.has(UNNAMED_LOCATION) || upChecks.has(UNNAMED_LOCATION)) && ( + + }} + /> + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx new file mode 100644 index 0000000000000..90aa887a78356 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { EuiHealth, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { UptimeSettingsContext } from '../../../../contexts'; +import { UNNAMED_LOCATION, UP } from './monitor_status_list'; + +interface MonitorStatusRowProps { + /** + * Recent List of checks performed on monitor + */ + locationNames: Set; + /** + * Monitor status for this of locations + */ + status: string; +} + +export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProps) => { + const { + colors: { success, danger }, + } = useContext(UptimeSettingsContext); + + const color = status === UP ? success : danger; + + let checkListArray = [...locationNames]; + // If un-named location exists, move it to end + if (locationNames.has(UNNAMED_LOCATION)) { + checkListArray = checkListArray.filter(item => item !== UNNAMED_LOCATION); + checkListArray.push(UNNAMED_LOCATION); + } + + if (locationNames.size === 0) { + return null; + } + + const locations = checkListArray.join(', '); + return ( + <> + + {status === UP ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx new file mode 100644 index 0000000000000..12ea8c52adeb0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.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 from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MonitorPageLink } from '../monitor_page_link'; +import { useUrlParams } from '../../../../hooks'; +import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; + +interface RecentError { + message: string; + type: string; +} + +interface MostRecentErrorProps { + /** + * error returned from API for monitor details + */ + error: RecentError; + + /** + * monitorId to be used for link to detail page + */ + monitorId: string; +} + +export const MostRecentError = ({ error, monitorId }: MostRecentErrorProps) => { + const [getUrlParams] = useUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); + params.selectedPingStatus = 'down'; + const linkParameters = stringifyUrlParams(params); + + return ( + <> + +

+ {i18n.translate('xpack.uptime.monitorList.mostRecentError.title', { + defaultMessage: 'Most recent error', + description: 'Most Recent Error title in Monitor List Expanded row', + })} +

+
+ + {error.message} + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_page_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_page_link.tsx new file mode 100644 index 0000000000000..803b399810508 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_page_link.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLink } from '@elastic/eui'; +import { Link } from 'react-router-dom'; +import React, { FunctionComponent } from 'react'; + +interface DetailPageLinkProps { + /** + * MonitorId to be used to redirect to detail page + */ + monitorId: string; + /** + * Link parameters usually filter states + */ + linkParameters: string | undefined; +} + +export const MonitorPageLink: FunctionComponent = ({ + children, + monitorId, + linkParameters, +}) => { + const getLocationTo = () => { + // encode monitorId param as 64 base string to make it a valid URL, since it can be a url + return linkParameters + ? `/monitor/${btoa(monitorId)}/${linkParameters}` + : `/monitor/${btoa(monitorId)}`; + }; + return ( + + + {children} + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/to_condensed_check.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/to_condensed_check.ts deleted file mode 100644 index 18bfe19f2fd5f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/to_condensed_check.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { Check } from '../../../../common/graphql/types'; -import { CondensedCheck } from './types'; - -const inferCondensedFields = ( - check: CondensedCheck, - currentStatus: string, - currentTimestamp: string -) => { - const { status: condensedStatus, timestamp } = check; - if (condensedStatus !== currentStatus && condensedStatus !== 'mixed') { - check.status = 'mixed'; - } - if (timestamp < currentTimestamp) { - check.timestamp = currentTimestamp; - } -}; - -export const toCondensedCheck = (checks: Check[]) => { - const condensedChecks: Map = new Map(); - checks.forEach((check: Check) => { - const location = get(check, 'observer.geo.name', null); - const { - monitor: { ip, status }, - timestamp, - } = check; - let condensedCheck: CondensedCheck | undefined; - if ((condensedCheck = condensedChecks.get(location))) { - condensedCheck.childStatuses.push({ ip, status, timestamp }); - inferCondensedFields(condensedCheck, status, timestamp); - } else { - condensedChecks.set(location, { - childStatuses: [{ ip, status, timestamp }], - location, - status, - timestamp, - }); - } - }); - return Array.from(condensedChecks.values()); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx deleted file mode 100644 index 4bfa2f95c3f77..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_page_link.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiLink } from '@elastic/eui'; -import { Link } from 'react-router-dom'; -import React, { FunctionComponent } from 'react'; - -interface DetailPageLinkProps { - id: string; - location: string | undefined; - linkParameters: string | undefined; -} - -export const MonitorPageLink: FunctionComponent = ({ - children, - id, - location, - linkParameters, -}) => ( - - - {children} - - -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index ddc6df14c2ade..e0d282a5348a0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,46 +5,69 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import { get } from 'lodash'; +import { connect } from 'react-redux'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { Snapshot as SnapshotType } from '../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { snapshotQuery } from '../../queries'; +import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; +import { AppState } from '../../state'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -interface SnapshotQueryResult { - snapshot?: SnapshotType; -} - -interface SnapshotProps { +/** + * Props expected from parent components. + */ +interface OwnProps { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; /** * Height is needed, since by default charts takes height of 100% */ height?: string; + statusFilter?: string; } -export type SnapshotComponentProps = SnapshotProps & UptimeGraphQLQueryProps; +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} /** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component + * Contains functions that will dispatch actions used + * for this component's lifecyclel + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. */ -export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentProps) => ( +type Props = OwnProps & StoreProps & DispatchProps; + +type PresentationalComponentProps = Pick & + Pick; + +export const PresentationalComponent: React.FC = ({ + count, + height, + loading, +}) => ( - (data, 'snapshot.counts.down', 0)} - total={get(data, 'snapshot.counts.total', 0)} - /> + (count, 'down', 0)} total={get(count, 'total', 0)} /> (data, 'snapshot.counts.up', 0)} - down={get(data, 'snapshot.counts.down', 0)} + up={get(count, 'up', 0)} + down={get(count, 'down', 0)} height={SNAPSHOT_CHART_HEIGHT} width={SNAPSHOT_CHART_WIDTH} /> @@ -54,8 +77,55 @@ export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentPr /** * This component visualizes a KPI and histogram chart to help users quickly * glean the status of their uptime environment. + * @param props the props required by the component */ -export const Snapshot = withUptimeGraphQL( - SnapshotComponent, - snapshotQuery -); +export const Container: React.FC = ({ + count, + dateRangeStart, + dateRangeEnd, + filters, + height, + statusFilter, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index a58d06ece0ede..b74bc943dc3eb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -12,6 +12,10 @@ import { Snapshot } from './snapshot'; interface StatusPanelProps { absoluteDateRangeStart: number; absoluteDateRangeEnd: number; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; sharedProps: { [key: string]: any }; } @@ -20,12 +24,22 @@ const STATUS_CHART_HEIGHT = '160px'; export const StatusPanel = ({ absoluteDateRangeStart, absoluteDateRangeEnd, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, sharedProps, }: StatusPanelProps) => ( - + { updateUrl({ dateRangeStart: start, dateRangeEnd: end }); refreshApp(); }} - // @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet onRefresh={refreshApp} onRefreshChange={({ isPaused, refreshInterval }: SuperDateRangePickerRefreshChangedEvent) => { updateUrl({ diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/__snapshots__/kibana_global_help.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/__snapshots__/kibana_global_help.test.tsx.snap deleted file mode 100644 index b3f749b12d9d1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/__snapshots__/kibana_global_help.test.tsx.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renderUptimeKibanaGlobalHelp renders links with expected urls 1`] = ` - - - - - For Uptime specific information - - - - - - - - - - -`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/kibana_global_help.test.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/kibana_global_help.test.tsx deleted file mode 100644 index a4791a41e0347..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/__tests__/kibana_global_help.test.tsx +++ /dev/null @@ -1,13 +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 { renderUptimeKibanaGlobalHelp } from '../kibana_global_help'; - -describe('renderUptimeKibanaGlobalHelp', () => { - it('renders links with expected urls', () => { - expect(renderUptimeKibanaGlobalHelp('https://elastic.co/', 'master')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx deleted file mode 100644 index 8e730dcc29310..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiLink, EuiSpacer, EuiHorizontalRule, EuiButton, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -export const renderUptimeKibanaGlobalHelp = (docsSiteUrl: string, docLinkVersion: string) => ( - - - - For Uptime specific information - - - - - - - - - -); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 44191d7e61e0d..94bfe79a6ca6e 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -9,12 +9,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; import { AutocompleteProviderRegister } from 'src/plugins/data/public'; +import { i18n as i18nFormatter } from '@kbn/i18n'; import { CreateGraphQLClient } from './framework_adapter_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; import { INTEGRATED_SOLUTIONS, PLUGIN } from '../../../../common/constants'; import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry'; -import { renderUptimeKibanaGlobalHelp } from './kibana_global_help'; import { UMFrameworkAdapter, BootstrapUptimeApp } from '../../lib'; import { createApolloClient } from './apollo_client_adapter'; @@ -52,12 +52,20 @@ export const getKibanaFrameworkAdapter = ( logMonitorPageLoad: getTelemetryMonitorPageLogger('true', basePath.get()), logOverviewPageLoad: getTelemetryOverviewPageLogger('true', basePath.get()), renderGlobalHelpControls: () => - setHelpExtension((element: HTMLElement) => { - ReactDOM.render( - renderUptimeKibanaGlobalHelp(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION), - element - ); - return () => ReactDOM.unmountComponentAtNode(element); + setHelpExtension({ + appName: i18nFormatter.translate('xpack.uptime.header.appName', { + defaultMessage: 'Uptime', + }), + links: [ + { + linkType: 'documentation', + href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-uptime.html`, + }, + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/uptime', + }, + ], }), routerBasename: basePath.prepend(PLUGIN.ROUTER_BASE_NAME), setBadge, diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ded16c3f8eb2f..561cc934a9b76 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -6,10 +6,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { EmptyState, @@ -26,6 +24,7 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getIndexPattern } from '../lib/adapters/index_pattern'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; +import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; interface OverviewPageProps { basePath: string; @@ -109,8 +108,8 @@ export const OverviewPage = ({ if (indexPattern) { const staticIndexPattern = toStaticIndexPattern(indexPattern); const combinedFilterString = combineFiltersAndUserSearch(filterQueryString, kueryString); - const ast = fromKueryExpression(combinedFilterString); - const elasticsearchQuery = toElasticsearchQuery(ast, staticIndexPattern); + const ast = esKuery.fromKueryExpression(combinedFilterString); + const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern); filters = JSON.stringify(elasticsearchQuery); } } @@ -151,6 +150,10 @@ export const OverviewPage = ({ diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts index d680ec6c543c4..b86522c03aba8 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/index.ts @@ -10,4 +10,3 @@ export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_q export { monitorPageTitleQuery } from './monitor_page_title_query'; export { monitorStatusBarQuery, monitorStatusBarQueryString } from './monitor_status_bar_query'; export { pingsQuery, pingsQueryString } from './pings_query'; -export { snapshotQuery, snapshotQueryString } from './snapshot_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts b/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts deleted file mode 100644 index 2db226876d220..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts +++ /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 gql from 'graphql-tag'; - -export const snapshotQueryString = ` -query Snapshot( - $dateRangeStart: String! - $dateRangeEnd: String! - $filters: String - $statusFilter: String -) { - snapshot: getSnapshot( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - filters: $filters - statusFilter: $statusFilter - ) { - counts { - down - mixed - up - total - } - } -} -`; - -export const snapshotQuery = gql` - ${snapshotQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index 1a33812ca8566..6b896b07bb066 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './snapshot'; export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts new file mode 100644 index 0000000000000..40738740e5841 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const FETCH_MONITOR_DETAILS = 'FETCH_MONITOR_DETAILS'; +export const FETCH_MONITOR_DETAILS_SUCCESS = 'FETCH_MONITOR_DETAILS_SUCCESS'; +export const FETCH_MONITOR_DETAILS_FAIL = 'FETCH_MONITOR_DETAILS_FAIL'; + +export interface MonitorDetailsState { + monitorId: string; + error: Error; +} + +interface GetMonitorDetailsAction { + type: typeof FETCH_MONITOR_DETAILS; + payload: string; +} + +interface GetMonitorDetailsSuccessAction { + type: typeof FETCH_MONITOR_DETAILS_SUCCESS; + payload: MonitorDetailsState; +} + +interface GetMonitorDetailsFailAction { + type: typeof FETCH_MONITOR_DETAILS_FAIL; + payload: any; +} + +export function fetchMonitorDetails(monitorId: string): GetMonitorDetailsAction { + return { + type: FETCH_MONITOR_DETAILS, + payload: monitorId, + }; +} + +export function fetchMonitorDetailsSuccess( + monitorDetailsState: MonitorDetailsState +): GetMonitorDetailsSuccessAction { + return { + type: FETCH_MONITOR_DETAILS_SUCCESS, + payload: monitorDetailsState, + }; +} + +export function fetchMonitorDetailsFail(error: any): GetMonitorDetailsFailAction { + return { + type: FETCH_MONITOR_DETAILS_FAIL, + payload: error, + }; +} + +export type MonitorActionTypes = + | GetMonitorDetailsAction + | GetMonitorDetailsSuccessAction + | GetMonitorDetailsFailAction; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts new file mode 100644 index 0000000000000..fe87a6a5960ee --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Snapshot } from '../../../common/runtime_types'; +export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT'; +export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL'; +export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS'; + +export interface GetSnapshotPayload { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +interface GetSnapshotCountFetchAction { + type: typeof FETCH_SNAPSHOT_COUNT; + payload: GetSnapshotPayload; +} + +interface GetSnapshotCountSuccessAction { + type: typeof FETCH_SNAPSHOT_COUNT_SUCCESS; + payload: Snapshot; +} + +interface GetSnapshotCountFailAction { + type: typeof FETCH_SNAPSHOT_COUNT_FAIL; + payload: Error; +} + +export type SnapshotActionTypes = + | GetSnapshotCountFetchAction + | GetSnapshotCountSuccessAction + | GetSnapshotCountFailAction; + +export const fetchSnapshotCount = ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string +): GetSnapshotCountFetchAction => ({ + type: FETCH_SNAPSHOT_COUNT, + payload: { + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }, +}); + +export const fetchSnapshotCountFail = (error: Error): GetSnapshotCountFailAction => ({ + type: FETCH_SNAPSHOT_COUNT_FAIL, + payload: error, +}); + +export const fetchSnapshotCountSuccess = (snapshot: Snapshot) => ({ + type: FETCH_SNAPSHOT_COUNT_SUCCESS, + payload: snapshot, +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts index 7c18774e1d67d..0bb2d8447419b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts @@ -5,17 +5,34 @@ */ export const SET_INTEGRATION_POPOVER_STATE = 'SET_INTEGRATION_POPOVER_STATE'; +export const SET_BASE_PATH = 'SET_BASE_PATH'; +export const REFRESH_APP = 'REFRESH_APP'; export interface PopoverState { id: string; open: boolean; } +interface SetBasePathAction { + type: typeof SET_BASE_PATH; + payload: string; +} + interface SetIntegrationPopoverAction { type: typeof SET_INTEGRATION_POPOVER_STATE; payload: PopoverState; } +interface TriggerAppRefreshAction { + type: typeof REFRESH_APP; + payload: number; +} + +export type UiActionTypes = + | SetIntegrationPopoverAction + | SetBasePathAction + | TriggerAppRefreshAction; + export function toggleIntegrationsPopover(popoverState: PopoverState): SetIntegrationPopoverAction { return { type: SET_INTEGRATION_POPOVER_STATE, @@ -23,4 +40,16 @@ export function toggleIntegrationsPopover(popoverState: PopoverState): SetIntegr }; } -export type UiActionTypes = SetIntegrationPopoverAction; +export function setBasePath(basePath: string): SetBasePathAction { + return { + type: SET_BASE_PATH, + payload: basePath, + }; +} + +export function triggerAppRefresh(refreshTime: number): TriggerAppRefreshAction { + return { + type: REFRESH_APP, + payload: refreshTime, + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..53716681664c2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = ` +[Error: Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/down: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/mixed: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/total: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/up: number] +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..f5fdfb172bc58 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchSnapshotCount } from '../snapshot'; + +describe('snapshot API', () => { + let fetchMock: jest.SpyInstance>>; + let mockResponse: Partial; + + beforeEach(() => { + fetchMock = jest.spyOn(window, 'fetch'); + mockResponse = { + ok: true, + json: () => new Promise(r => r({ up: 3, down: 12, mixed: 0, total: 15 })), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls url with expected params and returns response body on 200', async () => { + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + const resp = await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"', + statusFilter: 'up', + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up' + ); + expect(resp).toEqual({ up: 3, down: 12, mixed: 0, total: 15 }); + }); + + it(`throws when server response doesn't correspond to expected type`, async () => { + mockResponse = { ok: true, json: () => new Promise(r => r({ foo: 'bar' })) }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id: baz', + statusFilter: 'up', + }); + } catch (e) { + error = e; + } + expect(error).toMatchSnapshot(); + }); + + it('throws an error when response is not ok', async () => { + mockResponse = { ok: false, statusText: 'There was an error fetching your data.' }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }); + } catch (e) { + error = e; + } + expect(error).toEqual(new Error('There was an error fetching your data.')); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts new file mode 100644 index 0000000000000..a4429868494f1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './monitor'; +export * from './snapshot'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts new file mode 100644 index 0000000000000..d043cf7119472 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { getApiPath } from '../../lib/helper'; +import { MonitorDetailsType, MonitorDetails } from '../../../common/runtime_types'; + +interface ApiRequest { + monitorId: string; + basePath: string; +} + +export const fetchMonitorDetails = async ({ + monitorId, + basePath, +}: ApiRequest): Promise => { + const url = getApiPath(`/api/uptime/monitor/details?monitorId=${monitorId}`, basePath); + const response = await fetch(url); + if (!response.ok) { + throw new Error(response.statusText); + } + return response.json().then(data => { + ThrowReporter.report(MonitorDetailsType.decode(data)); + return data; + }); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts new file mode 100644 index 0000000000000..cbfe00a4a8746 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { getApiPath } from '../../lib/helper'; +import { SnapshotType, Snapshot } from '../../../common/runtime_types'; + +interface ApiRequest { + basePath: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +export const fetchSnapshotCount = async ({ + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, +}: ApiRequest): Promise => { + const url = getApiPath(`/api/uptime/snapshot/count`, basePath); + const params = { + dateRangeStart, + dateRangeEnd, + ...(filters && { filters }), + ...(statusFilter && { statusFilter }), + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + const decoded = SnapshotType.decode(responseData); + ThrowReporter.report(decoded); + if (isRight(decoded)) { + return decoded.right; + } + throw new Error('`getSnapshotCount` response did not correspond to expected type'); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts new file mode 100644 index 0000000000000..4eb027d642974 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fork } from 'redux-saga/effects'; +import { fetchMonitorDetailsEffect } from './monitor'; +import { fetchSnapshotCountSaga } from './snapshot'; + +export function* rootEffect() { + yield fork(fetchMonitorDetailsEffect); + yield fork(fetchSnapshotCountSaga); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts new file mode 100644 index 0000000000000..529b9041c9093 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.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 { call, put, takeLatest, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { + FETCH_MONITOR_DETAILS, + FETCH_MONITOR_DETAILS_SUCCESS, + FETCH_MONITOR_DETAILS_FAIL, +} from '../actions/monitor'; +import { fetchMonitorDetails } from '../api'; +import { getBasePath } from '../selectors'; + +function* monitorDetailsEffect(action: Action) { + const monitorId: string = action.payload; + try { + const basePath = yield select(getBasePath); + const response = yield call(fetchMonitorDetails, { monitorId, basePath }); + yield put({ type: FETCH_MONITOR_DETAILS_SUCCESS, payload: response }); + } catch (error) { + yield put({ type: FETCH_MONITOR_DETAILS_FAIL, payload: error.message }); + } +} + +export function* fetchMonitorDetailsEffect() { + yield takeLatest(FETCH_MONITOR_DETAILS, monitorDetailsEffect); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts new file mode 100644 index 0000000000000..23ac1016d2244 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { call, put, takeLatest, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { + FETCH_SNAPSHOT_COUNT, + GetSnapshotPayload, + fetchSnapshotCountFail, + fetchSnapshotCountSuccess, +} from '../actions'; +import { fetchSnapshotCount } from '../api'; +import { getBasePath } from '../selectors'; + +function* snapshotSaga(action: Action) { + try { + if (!action.payload) { + yield put( + fetchSnapshotCountFail(new Error('Cannot fetch snapshot for undefined parameters.')) + ); + return; + } + const { + payload: { dateRangeStart, dateRangeEnd, filters, statusFilter }, + } = action; + const basePath = yield select(getBasePath); + const response = yield call(fetchSnapshotCount, { + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }); + yield put(fetchSnapshotCountSuccess(response)); + } catch (error) { + yield put(fetchSnapshotCountFail(error)); + } +} + +export function* fetchSnapshotCountSaga() { + yield takeLatest(FETCH_SNAPSHOT_COUNT, snapshotSaga); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/index.ts b/x-pack/legacy/plugins/uptime/public/state/index.ts index 4ef3d26776a7e..e3563c74294d2 100644 --- a/x-pack/legacy/plugins/uptime/public/state/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/index.ts @@ -3,12 +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 { compose, createStore } from 'redux'; +import { compose, createStore, applyMiddleware } from 'redux'; +import createSagaMiddleware from 'redux-saga'; +import { rootEffect } from './effects'; import { rootReducer } from './reducers'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -export const store = createStore(rootReducer, composeEnhancers()); +const sagaMW = createSagaMiddleware(); + +export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW))); export type AppState = ReturnType; + +sagaMW.run(rootEffect); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..d3a21ec9eece3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot reducer appends a current error to existing errors list 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [ + [Error: I couldn't get your data because the server denied the request], + ], + "loading": false, +} +`; + +exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] = ` +Object { + "count": Object { + "down": 15, + "mixed": 0, + "total": 25, + "up": 10, + }, + "errors": Array [], + "loading": false, +} +`; + +exports[`snapshot reducer sets the state's status to loading during a fetch 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [], + "loading": true, +} +`; + +exports[`snapshot reducer updates existing state 1`] = ` +Object { + "count": Object { + "down": 1, + "mixed": 0, + "total": 4, + "up": 3, + }, + "errors": Array [], + "loading": true, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap new file mode 100644 index 0000000000000..75516da18c633 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ui reducer adds integration popover status to state 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": Object { + "id": "popover-2", + "open": true, + }, + "lastRefresh": 125, +} +`; + +exports[`ui reducer sets the application's base path 1`] = ` +Object { + "basePath": "yyz", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; + +exports[`ui reducer updates the refresh value 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..a4b317d5af197 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { snapshotReducer } from '../snapshot'; +import { SnapshotActionTypes } from '../../actions'; + +describe('snapshot reducer', () => { + it('updates existing state', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'foo: bar', + statusFilter: 'up', + }, + }; + expect( + snapshotReducer( + { + count: { down: 1, mixed: 0, total: 4, up: 3 }, + errors: [], + loading: false, + }, + action + ) + ).toMatchSnapshot(); + }); + + it(`sets the state's status to loading during a fetch`, () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('changes the count when a snapshot fetch succeeds', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_SUCCESS', + payload: { + up: 10, + down: 15, + mixed: 0, + total: 25, + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('appends a current error to existing errors list', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_FAIL', + payload: new Error(`I couldn't get your data because the server denied the request`), + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts new file mode 100644 index 0000000000000..9be863f0b700d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionTypes } from '../../actions'; +import { uiReducer } from '../ui'; + +describe('ui reducer', () => { + it(`sets the application's base path`, () => { + const action: UiActionTypes = { + type: 'SET_BASE_PATH', + payload: 'yyz', + }; + expect( + uiReducer( + { + basePath: 'abc', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('adds integration popover status to state', () => { + const action: UiActionTypes = { + type: 'SET_INTEGRATION_POPOVER_STATE', + payload: { + id: 'popover-2', + open: true, + }, + }; + expect( + uiReducer( + { + basePath: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('updates the refresh value', () => { + const action: UiActionTypes = { + type: 'REFRESH_APP', + payload: 125, + }; + expect(uiReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index faa932a321cd1..f0c3d1c2cbecf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -5,9 +5,12 @@ */ import { combineReducers } from 'redux'; - +import { monitorReducer } from './monitor'; +import { snapshotReducer } from './snapshot'; import { uiReducer } from './ui'; export const rootReducer = combineReducers({ + monitor: monitorReducer, + snapshot: snapshotReducer, ui: uiReducer, }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts new file mode 100644 index 0000000000000..4cacb6f8cab9e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MonitorActionTypes, + MonitorDetailsState, + FETCH_MONITOR_DETAILS, + FETCH_MONITOR_DETAILS_SUCCESS, + FETCH_MONITOR_DETAILS_FAIL, +} from '../actions/monitor'; + +export interface MonitorState { + monitorDetailsList: MonitorDetailsState[]; + loading: boolean; + errors: any[]; +} + +const initialState: MonitorState = { + monitorDetailsList: [], + loading: false, + errors: [], +}; + +export function monitorReducer(state = initialState, action: MonitorActionTypes): MonitorState { + switch (action.type) { + case FETCH_MONITOR_DETAILS: + return { + ...state, + loading: true, + }; + case FETCH_MONITOR_DETAILS_SUCCESS: + const { monitorId } = action.payload; + return { + ...state, + monitorDetailsList: { + ...state.monitorDetailsList, + [monitorId]: action.payload, + }, + loading: false, + }; + case FETCH_MONITOR_DETAILS_FAIL: + const error = action.payload; + return { + ...state, + errors: [...state.errors, error], + }; + default: + return state; + } +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts new file mode 100644 index 0000000000000..dd9449325f4fb --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Snapshot } from '../../../common/runtime_types'; +import { + FETCH_SNAPSHOT_COUNT, + FETCH_SNAPSHOT_COUNT_FAIL, + FETCH_SNAPSHOT_COUNT_SUCCESS, + SnapshotActionTypes, +} from '../actions'; + +export interface SnapshotState { + count: Snapshot; + errors: any[]; + loading: boolean; +} + +const initialState: SnapshotState = { + count: { + down: 0, + mixed: 0, + total: 0, + up: 0, + }, + errors: [], + loading: false, +}; + +export function snapshotReducer(state = initialState, action: SnapshotActionTypes): SnapshotState { + switch (action.type) { + case FETCH_SNAPSHOT_COUNT: + return { + ...state, + loading: true, + }; + case FETCH_SNAPSHOT_COUNT_SUCCESS: + return { + ...state, + count: action.payload, + loading: false, + }; + case FETCH_SNAPSHOT_COUNT_FAIL: + return { + ...state, + errors: [...state.errors, action.payload], + }; + default: + return state; + } +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts index 1550b6c9936c3..be95c8fff6bec 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts @@ -4,18 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UiActionTypes, PopoverState, SET_INTEGRATION_POPOVER_STATE } from '../actions/ui'; +import { + UiActionTypes, + PopoverState, + SET_INTEGRATION_POPOVER_STATE, + SET_BASE_PATH, + REFRESH_APP, +} from '../actions/ui'; export interface UiState { integrationsPopoverOpen: PopoverState | null; + basePath: string; + lastRefresh: number; } const initialState: UiState = { integrationsPopoverOpen: null, + basePath: '', + lastRefresh: Date.now(), }; export function uiReducer(state = initialState, action: UiActionTypes): UiState { switch (action.type) { + case REFRESH_APP: + return { + ...state, + lastRefresh: action.payload, + }; case SET_INTEGRATION_POPOVER_STATE: const popoverState = action.payload; return { @@ -25,6 +40,12 @@ export function uiReducer(state = initialState, action: UiActionTypes): UiState open: popoverState.open, }, }; + case SET_BASE_PATH: + const basePath = action.payload; + return { + ...state, + basePath, + }; default: return state; } diff --git a/x-pack/legacy/plugins/uptime/public/state/sagas/index.ts b/x-pack/legacy/plugins/uptime/public/state/sagas/index.ts deleted file mode 100644 index 7bb3c694f5120..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/sagas/index.ts +++ /dev/null @@ -1,11 +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 { fork } from 'redux-saga/effects'; - -// export function* rootSaga() { -// yield fork(); -// } diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts new file mode 100644 index 0000000000000..70cd2b19860ba --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBasePath, isIntegrationsPopupOpen } from '../index'; +import { AppState } from '../../../state'; + +describe('state selectors', () => { + const state: AppState = { + monitor: { + monitorDetailsList: [], + loading: false, + errors: [], + }, + snapshot: { + count: { + up: 2, + down: 0, + mixed: 0, + total: 2, + }, + errors: [], + loading: false, + }, + ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 }, + }; + + it('selects base path from state', () => { + expect(getBasePath(state)).toBe('yyz'); + }); + + it('gets integrations popup state', () => { + const integrationsPopupOpen = { + id: 'popup-id', + open: true, + }; + state.ui.integrationsPopoverOpen = integrationsPopupOpen; + expect(isIntegrationsPopupOpen(state)).toBe(integrationsPopupOpen); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 223e7c029cb1c..245b45a939950 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -3,6 +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 { AppState } from '../../state'; -export const isIntegrationsPopupOpen = (state: AppState) => state.ui.integrationsPopoverOpen; +export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; + +export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => + integrationsPopoverOpen; + +export const getMonitorDetails = (state: AppState, summary: any) => { + return state.monitor.monitorDetailsList[summary.monitor_id]; +}; diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 5defd834f5d2d..47743729c1e76 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -22,6 +22,7 @@ import { UptimeDatePicker } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; import { getTitle } from './lib/helper/get_title'; import { store } from './state'; +import { setBasePath, triggerAppRefresh } from './state/actions'; export interface UptimeAppColors { danger: string; @@ -115,7 +116,9 @@ const Application = (props: UptimeAppProps) => { }, []); const refreshApp = () => { - setLastRefresh(Date.now()); + const refreshTime = Date.now(); + setLastRefresh(refreshTime); + store.dispatch(triggerAppRefresh(refreshTime)); }; const [getUrlParams] = useUrlParams(); @@ -146,6 +149,8 @@ const Application = (props: UptimeAppProps) => { }; }; + store.dispatch(setBasePath(basePath)); + return ( diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index 96a386b6a6848..415afc87e201e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -12,24 +12,15 @@ import { GetLatestMonitorsQueryArgs, GetMonitorChartsDataQueryArgs, GetMonitorPageTitleQueryArgs, - GetSnapshotQueryArgs, MonitorChart, MonitorPageTitle, Ping, - Snapshot, GetSnapshotHistogramQueryArgs, } from '../../../common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; import { HistogramResult } from '../../../common/domain_types'; -export type UMSnapshotResolver = UMResolver< - Snapshot | Promise, - any, - GetSnapshotQueryArgs, - UMContext ->; - export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; export type UMLatestMonitorsResolver = UMResolver< @@ -71,7 +62,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( libs: UMServerLibs ): { Query: { - getSnapshot: UMSnapshotResolver; getSnapshotHistogram: UMGetSnapshotHistogram; getMonitorChartsData: UMGetMonitorChartsResolver; getLatestMonitors: UMLatestMonitorsResolver; @@ -80,23 +70,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( }; } => ({ Query: { - async getSnapshot( - resolver, - { dateRangeStart, dateRangeEnd, filters, statusFilter }, - { req } - ): Promise { - const counts = await libs.monitors.getSnapshotCount( - req, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter - ); - - return { - counts, - }; - }, async getSnapshotHistogram( resolver, { dateRangeStart, dateRangeEnd, filters, monitorId, statusFilter }, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts index 97dcbd12fff2e..f9b14c63e70bb 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts @@ -31,17 +31,6 @@ export const monitorsSchema = gql` y: UnsignedInteger } - type SnapshotCount { - up: Int! - down: Int! - mixed: Int! - total: Int! - } - - type Snapshot { - counts: SnapshotCount! - } - type DataPoint { x: UnsignedInteger y: Float @@ -139,13 +128,6 @@ export const monitorsSchema = gql` statusFilter: String ): LatestMonitorsResult - getSnapshot( - dateRangeStart: String! - dateRangeEnd: String! - filters: String - statusFilter: String - ): Snapshot - getSnapshotHistogram( dateRangeStart: String! dateRangeEnd: String! diff --git a/x-pack/legacy/plugins/uptime/server/kibana.index.ts b/x-pack/legacy/plugins/uptime/server/kibana.index.ts index 874fb2e37e902..73fabc629946b 100644 --- a/x-pack/legacy/plugins/uptime/server/kibana.index.ts +++ b/x-pack/legacy/plugins/uptime/server/kibana.index.ts @@ -22,17 +22,13 @@ export interface KibanaRouteOptions { export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; - usage: { - collectorSet: { - register: (usageCollector: any) => any; - }; - }; } export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { - const { usageCollector, xpack } = plugins; + const { usageCollection, xpack } = plugins; const libs = compose(server, plugins); - usageCollector.collectorSet.register(KibanaTelemetryAdapter.initUsageCollector(usageCollector)); + KibanaTelemetryAdapter.registerUsageCollector(usageCollection); + initUptimeServer(libs); xpack.registerFeature({ diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index a31b4f99c522a..df2723283f88c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { Lifecycle, ResponseToolkit } from 'hapi'; import { RouteOptions } from 'hapi'; import { SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; export interface UMFrameworkRequest { user: string; @@ -37,7 +38,7 @@ export interface UptimeCoreSetup { export interface UptimeCorePlugins { elasticsearch: any; savedObjects: SavedObjectsLegacyService; - usageCollector: any; + usageCollection: UsageCollectionSetup; xpack: any; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap new file mode 100644 index 0000000000000..29c82ff455d36 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`get snapshot helper reduces check groups as expected 1`] = ` +Object { + "down": 1, + "mixed": 0, + "total": 3, + "up": 2, +} +`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json deleted file mode 100644 index bd4248755095d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "bool": { - "filter": [ - { - "bool": { - "filter": [ - { - "bool": { - "should": [{ "match_phrase": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match_phrase": { "monitor.name": "" } }], - "minimum_should_match": 1 - } - } - ] - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0000" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0002" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0003" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0004" } }], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ] - } -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts new file mode 100644 index 0000000000000..917e4a149de67 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { getSnapshotCountHelper } from '../get_snapshot_helper'; +import { MonitorGroups } from '../search'; + +describe('get snapshot helper', () => { + let mockIterator: any; + beforeAll(() => { + mockIterator = jest.fn(); + const summaryTimestamp = new Date('2019-01-01'); + const firstResult: MonitorGroups = { + id: 'firstGroup', + groups: [ + { + monitorId: 'first-monitor', + location: 'us-east-1', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'us-west-1', + checkGroup: 'abc', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'amsterdam', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + ], + }; + const secondResult: MonitorGroups = { + id: 'secondGroup', + groups: [ + { + monitorId: 'second-monitor', + location: 'us-east-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'us-west-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'amsterdam', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + ], + }; + const thirdResult: MonitorGroups = { + id: 'thirdGroup', + groups: [ + { + monitorId: 'third-monitor', + location: 'us-east-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'us-west-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'amsterdam', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + ], + }; + + const mockNext = jest + .fn() + .mockReturnValueOnce(firstResult) + .mockReturnValueOnce(secondResult) + .mockReturnValueOnce(thirdResult) + .mockReturnValueOnce(null); + mockIterator.next = mockNext; + }); + + it('reduces check groups as expected', async () => { + expect(await getSnapshotCountHelper(mockIterator)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts index 781f30314d350..57b1744f5d324 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts @@ -21,6 +21,13 @@ export interface UMMonitorStatesAdapter { statusFilter?: string | null ): Promise; statesIndexExists(request: any): Promise; + getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise; } export interface CursorPagination { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts index 59c3e022e7d04..c3593854fa53f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts @@ -9,6 +9,9 @@ import { UMMonitorStatesAdapter, GetMonitorStatesResult, CursorPagination } from import { StatesIndexStatus } from '../../../../common/graphql/types'; import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants'; import { fetchPage } from './search'; +import { MonitorGroupIterator } from './search/monitor_group_iterator'; +import { Snapshot } from '../../../../common/runtime_types'; +import { getSnapshotCountHelper } from './get_snapshot_helper'; export interface QueryContext { database: any; @@ -57,6 +60,26 @@ export class ElasticsearchMonitorStatesAdapter implements UMMonitorStatesAdapter }; } + public async getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise { + const context: QueryContext = { + database: this.database, + request, + dateRangeStart, + dateRangeEnd, + pagination: CONTEXT_DEFAULTS.CURSOR_PAGINATION, + filterClause: filters && filters !== '' ? JSON.parse(filters) : null, + size: CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT, + statusFilter, + }; + return getSnapshotCountHelper(new MonitorGroupIterator(context)); + } + public async statesIndexExists(request: any): Promise { // TODO: adapt this to the states index in future release const { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts new file mode 100644 index 0000000000000..8bd21b77406df --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MonitorGroups, MonitorGroupIterator } from './search'; +import { Snapshot } from '../../../../common/runtime_types'; + +const reduceItemsToCounts = (items: MonitorGroups[]) => { + let down = 0; + let up = 0; + items.forEach(item => { + if (item.groups.some(group => group.status === 'down')) { + down++; + } else { + up++; + } + }); + return { + down, + mixed: 0, + total: down + up, + up, + }; +}; + +export const getSnapshotCountHelper = async (iterator: MonitorGroupIterator): Promise => { + const items: MonitorGroups[] = []; + let res: MonitorGroups | null; + // query the index to find the most recent check group for each monitor/location + do { + res = await iterator.next(); + if (res) { + items.push(res); + } + } while (res !== null); + + return reduceItemsToCounts(items); +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts index 2fa2112161dcd..040c256935692 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts @@ -5,3 +5,4 @@ */ export { fetchPage, MonitorGroups, MonitorLocCheckGroup, MonitorGroupsPage } from './fetch_page'; +export { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts index 2fec58593e5d8..1de2dbb0e364d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts @@ -97,9 +97,12 @@ export class MonitorGroupIterator { } } - // Attempts to buffer more results fetching a single chunk. - // If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. - // to free up space. + /** + * Attempts to buffer more results fetching a single chunk. + * If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. + * to free up space. + * @param size the number of items to chunk + */ async attemptBufferMore( size: number = CHUNK_SIZE ): Promise<{ hasMore: boolean; gotHit: boolean }> { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts index 8c184c3356989..f6ac587b0ceec 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts @@ -14,13 +14,7 @@ export interface UMMonitorsAdapter { dateRangeEnd: string, location?: string | null ): Promise; - getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise; getFilterBar(request: any, dateRangeStart: string, dateRangeEnd: string): Promise; getMonitorPageTitle(request: any, monitorId: string): Promise; + getMonitorDetails(request: any, monitorId: string): Promise; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index f2d84d149344b..ef0837a043172 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, reduce } from 'lodash'; -import { INDEX_NAMES, QUERY } from '../../../../common/constants'; +import { get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; import { FilterBar, MonitorChart, @@ -13,9 +13,10 @@ import { Ping, LocationDurationLine, } from '../../../../common/graphql/types'; -import { getFilterClause, parseFilterQuery, getHistogramIntervalFormatted } from '../../helper'; +import { getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter } from '../database'; import { UMMonitorsAdapter } from './adapter_types'; +import { MonitorDetails, Error } from '../../../../common/runtime_types'; const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { let up = null; @@ -183,155 +184,6 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { return monitorChartsData; } - /** - * Provides a count of the current monitors - * @param request Kibana request - * @param dateRangeStart timestamp bounds - * @param dateRangeEnd timestamp bounds - * @param filters filters defined by client - */ - public async getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise { - const query = parseFilterQuery(filters); - const additionalFilters = [{ exists: { field: 'summary.up' } }]; - if (query) { - additionalFilters.push(query); - } - const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionalFilters); - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter, - }, - }, - size: 0, - aggs: { - ids: { - composite: { - sources: [ - { - id: { - terms: { - field: 'monitor.id', - }, - }, - }, - { - location: { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - }, - ], - size: QUERY.DEFAULT_AGGS_CAP, - }, - aggs: { - latest: { - top_hits: { - sort: [{ '@timestamp': { order: 'desc' } }], - _source: { - includes: ['summary.*', 'monitor.id', '@timestamp', 'observer.geo.name'], - }, - size: 1, - }, - }, - }, - }, - }, - }, - }; - - let searchAfter: any = null; - - const summaryByIdLocation: { - // ID - [key: string]: { - // Location - [key: string]: { up: number; down: number; timestamp: number }; - }; - } = {}; - - do { - if (searchAfter) { - set(params, 'body.aggs.ids.composite.after', searchAfter); - } - - const queryResult = await this.database.search(request, params); - const idBuckets = get(queryResult, 'aggregations.ids.buckets', []); - - idBuckets.forEach(bucket => { - // We only get the latest doc - const source: any = get(bucket, 'latest.hits.hits[0]._source'); - const { - summary: { up, down }, - monitor: { id }, - } = source; - const timestamp = get(source, '@timestamp', 0); - const location = get(source, 'observer.geo.name', ''); - - let idSummary = summaryByIdLocation[id]; - if (!idSummary) { - idSummary = {}; - summaryByIdLocation[id] = idSummary; - } - const locationSummary = idSummary[location]; - if (!locationSummary || locationSummary.timestamp < timestamp) { - idSummary[location] = { timestamp, up, down }; - } - }); - - searchAfter = get(queryResult, 'aggregations.ids.after_key'); - } while (searchAfter); - - let up: number = 0; - let mixed: number = 0; - let down: number = 0; - - for (const id in summaryByIdLocation) { - if (!summaryByIdLocation.hasOwnProperty(id)) { - continue; - } - const locationInfo = summaryByIdLocation[id]; - const { up: locationUp, down: locationDown } = reduce( - locationInfo, - (acc, value, key) => { - acc.up += value.up; - acc.down += value.down; - return acc; - }, - { up: 0, down: 0 } - ); - - if (locationDown === 0) { - up++; - } else if (locationUp > 0) { - mixed++; - } else { - down++; - } - } - - const result: any = { up, down, mixed, total: up + down + mixed }; - if (statusFilter) { - for (const status in result) { - if (status !== 'total' && status !== statusFilter) { - result[status] = 0; - } - } - } - - return result; - } - /** * Fetch options for the filter bar. * @param request Kibana request object @@ -420,4 +272,47 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { name: get(pageTitle, 'monitor.name', null), }; } + + public async getMonitorDetails(request: any, monitorId: string): Promise { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 1, + query: { + bool: { + must: [ + { + exists: { + field: 'error', + }, + }, + ], + filter: [ + { + term: { + 'monitor.id': monitorId, + }, + }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + const result = await this.database.search(request, params); + + const monitorError: Error | undefined = get(result, 'hits.hits[0]._source.error', undefined); + + return { + monitorId, + error: monitorError, + }; + } } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts index d8279cb3399bd..8e4011b4cf0eb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts @@ -7,21 +7,19 @@ import { KibanaTelemetryAdapter } from '../kibana_telemetry_adapter'; describe('KibanaTelemetryAdapter', () => { - let telemetry: any; + let usageCollection: any; let collector: { type: string; fetch: () => Promise; isReady: () => boolean }; beforeEach(() => { - telemetry = { - collectorSet: { - makeUsageCollector: (val: any) => { - collector = val; - }, + usageCollection = { + makeUsageCollector: (val: any) => { + collector = val; }, }; }); it('collects monitor and overview data', async () => { expect.assertions(1); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); KibanaTelemetryAdapter.countOverview(); @@ -33,7 +31,7 @@ describe('KibanaTelemetryAdapter', () => { expect.assertions(1); // give a time of > 24 hours ago Date.now = jest.fn(() => 1559053560000); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); // give a time of now @@ -47,7 +45,7 @@ describe('KibanaTelemetryAdapter', () => { }); it('defaults ready to `true`', async () => { - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); expect(collector.isReady()).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index a906c741c5241..8dec0c1d2d485 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; interface UptimeTelemetry { overview_page: number; @@ -19,9 +20,13 @@ const BUCKET_SIZE = 3600; const BUCKET_NUMBER = 24; export class KibanaTelemetryAdapter { - public static initUsageCollector(usageCollector: any) { - const { collectorSet } = usageCollector; - return collectorSet.makeUsageCollector({ + public static registerUsageCollector = (usageCollector: UsageCollectionSetup) => { + const collector = KibanaTelemetryAdapter.initUsageCollector(usageCollector); + usageCollector.registerCollector(collector); + }; + + public static initUsageCollector(usageCollector: UsageCollectionSetup) { + return usageCollector.makeUsageCollector({ type: 'uptime', fetch: async () => { const report = this.getReport(); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index 53ed7da4779a4..889f8a820b2f3 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -8,14 +8,18 @@ import { createIsValidRoute } from './auth'; import { createGetAllRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; +import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteCreator } from './types'; +import { createGetMonitorDetailsRoute } from './monitors'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export const restApiRoutes: UMRestApiRouteCreator[] = [ - createIsValidRoute, createGetAllRoute, + createGetIndexPatternRoute, + createGetMonitorDetailsRoute, + createGetSnapshotCount, + createIsValidRoute, createLogMonitorPageRoute, createLogOverviewPageRoute, - createGetIndexPatternRoute, ]; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts new file mode 100644 index 0000000000000..2c4b9e9fb1f3e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createGetMonitorDetailsRoute } from './monitors_details'; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts new file mode 100644 index 0000000000000..1440b55c1c137 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Joi from 'joi'; +import { UMServerLibs } from '../../lib/lib'; +import { MonitorDetails } from '../../../common/runtime_types/monitor/monitor_details'; + +export const createGetMonitorDetailsRoute = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/monitor/details', + options: { + validate: { + query: Joi.object({ + monitorId: Joi.string(), + }), + }, + tags: ['access:uptime'], + }, + handler: async (request: any): Promise => { + const { monitorId } = request.query; + return await libs.monitors.getMonitorDetails(request, monitorId); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts new file mode 100644 index 0000000000000..ddca622005d63 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { UMServerLibs } from '../../lib/lib'; +import { Snapshot } from '../../../common/runtime_types'; + +export const createGetSnapshotCount = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/snapshot/count', + options: { + validate: { + query: Joi.object({ + dateRangeStart: Joi.string().required(), + dateRangeEnd: Joi.string().required(), + filters: Joi.string(), + statusFilter: Joi.string(), + }), + }, + tags: ['access:uptime'], + }, + handler: async (request: any): Promise => { + const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; + return await libs.monitorStates.getSnapshotCount( + request, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter + ); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts new file mode 100644 index 0000000000000..934b34ef1b397 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createGetSnapshotCount } from './get_snapshot_count'; diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index d5753ef6f3c85..021464f32a203 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -23,8 +23,8 @@ export function settingsRoute(server, kbnServer) { const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request try { - const { collectorSet } = server.usage; - const settingsCollector = collectorSet.getCollectorByType(KIBANA_SETTINGS_TYPE); + const { usageCollection } = server.newPlatform.setup.plugins; + const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE); let settings = await settingsCollector.fetch(callCluster); if (!settings) { diff --git a/x-pack/package.json b/x-pack/package.json index 402c3e95159f3..c5114500c6f61 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -14,8 +14,7 @@ "test:browser:dev": "gulp testbrowser-dev", "test:browser": "gulp testbrowser", "test:jest": "node scripts/jest", - "test:mocha": "node scripts/mocha", - "test:server": "gulp testserver" + "test:mocha": "node scripts/mocha" }, "kibana": { "build": { @@ -93,12 +92,12 @@ "@types/react-resize-detector": "^4.0.1", "@types/react-router-dom": "^4.3.1", "@types/react-sticky": "^6.0.3", - "@types/react-test-renderer": "^16.8.0", + "@types/react-test-renderer": "^16.8.3", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", "@types/sinon": "^7.0.13", - "@types/styled-components": "^3.0.2", + "@types/styled-components": "^4.4.0", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", "@types/tinycolor2": "^1.4.1", @@ -116,11 +115,12 @@ "cheerio": "0.22.0", "commander": "3.0.0", "copy-webpack-plugin": "^5.0.4", - "cypress": "^3.5.5", + "cypress": "^3.6.1", + "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", - "enzyme-adapter-utils": "^1.12.0", - "enzyme-to-json": "^3.3.4", + "enzyme-adapter-react-16": "^1.15.1", + "enzyme-adapter-utils": "^1.12.1", + "enzyme-to-json": "^3.4.3", "execa": "^3.2.0", "fancy-log": "^1.3.2", "fetch-mock": "^7.3.9", @@ -132,16 +132,15 @@ "graphql-codegen-typescript-resolvers": "^0.18.2", "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", - "gulp-mocha": "^7.0.2", "hapi": "^17.5.3", "jest": "^24.9.0", "jest-cli": "^24.9.0", - "jest-styled-components": "^6.3.3", + "jest-styled-components": "^7.0.0-beta.2", "jsdom": "^12.2.0", "madge": "3.4.4", - "mocha": "6.2.1", + "marge": "^1.0.1", + "mocha": "^6.2.2", "mocha-junit-reporter": "^1.23.1", - "mocha-multi-reporters": "^1.1.7", "mochawesome": "^4.1.0", "mochawesome-merge": "^2.0.1", "mustache": "^2.3.0", @@ -154,7 +153,7 @@ "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-hooks-testing-library": "^0.3.8", - "react-test-renderer": "^16.8.0", + "react-test-renderer": "^16.8.6", "react-testing-library": "^6.0.0", "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", @@ -174,22 +173,18 @@ }, "dependencies": { "@babel/core": "^7.5.5", - "@babel/register": "^7.5.5", + "@babel/register": "^7.7.0", "@babel/runtime": "^7.5.5", - "@elastic/ctags-langserver": "^0.1.11", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "14.9.0", + "@elastic/eui": "16.0.0", "@elastic/filesaver": "1.1.2", - "@elastic/javascript-typescript-langserver": "^0.3.3", - "@elastic/lsp-extension": "^0.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", "@elastic/numeral": "2.3.3", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", @@ -214,6 +209,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", @@ -246,7 +242,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", @@ -297,11 +293,11 @@ "puid": "1.0.7", "puppeteer-core": "^1.19.0", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", "react-datetime": "^2.14.0", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", "react-markdown": "^3.4.1", @@ -334,7 +330,7 @@ "squel": "^5.13.0", "stats-lite": "^2.2.0", "style-it": "^2.1.3", - "styled-components": "3.4.10", + "styled-components": "beta", "suricata-sid-db": "^1.0.2", "tinycolor2": "1.4.1", "tinymath": "1.2.1", @@ -358,10 +354,9 @@ }, "workspaces": { "nohoist": [ - "**/mochawesome", - "**/mochawesome/**", - "**/mocha-multi-reporters", - "**/mocha-multi-reporters/**" + "mochawesome", + "mochawesome-merge", + "cypress-multi-reporters" ] } } diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index e2d1892b1355e..cc4a7c90de513 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_act import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction } from './custom_time_range_action'; @@ -24,12 +24,12 @@ import { CustomTimeRangeBadge } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; } diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json new file mode 100644 index 0000000000000..8161f6ee06bf8 --- /dev/null +++ b/x-pack/plugins/apm/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "apm", + "server": true, + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": [ + "xpack", + "apm" + ], + "ui": false, + "requiredPlugins": [ + "apm_oss" + ] +} diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts new file mode 100644 index 0000000000000..082216a78ce5e --- /dev/null +++ b/x-pack/plugins/apm/server/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from 'src/core/server'; +import { APMOSSConfig } from 'src/plugins/apm_oss/server'; +import { APMPlugin } from './plugin'; + +export const config = { + schema: schema.object({ + serviceMapEnabled: schema.boolean({ defaultValue: false }), + autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + transactionGroupBucketSize: schema.number({ defaultValue: 100 }), + maxTraceItems: schema.number({ defaultValue: 1000 }), + }), + }), +}; + +export type APMXPackConfig = TypeOf; + +export function mergeConfigs(apmOssConfig: APMOSSConfig, apmConfig: APMXPackConfig) { + return { + 'apm_oss.transactionIndices': apmOssConfig.transactionIndices, + 'apm_oss.spanIndices': apmOssConfig.spanIndices, + 'apm_oss.errorIndices': apmOssConfig.errorIndices, + 'apm_oss.metricsIndices': apmOssConfig.metricsIndices, + 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices, + 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, + 'apm_oss.indexPattern': apmOssConfig.indexPattern, + 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, + 'xpack.apm.ui.enabled': apmConfig.ui.enabled, + 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, + 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, + 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, + }; +} + +export type APMConfig = ReturnType; + +export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); + +export { APMPlugin } from './plugin'; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts new file mode 100644 index 0000000000000..b28e00adcc6d1 --- /dev/null +++ b/x-pack/plugins/apm/server/plugin.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { Observable, combineLatest, AsyncSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Server } from 'hapi'; +import { once } from 'lodash'; +import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server'; +import { createApmAgentConfigurationIndex } from '../../../legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index'; +import { createApmApi } from '../../../legacy/plugins/apm/server/routes/create_apm_api'; +import { APMConfig, mergeConfigs, APMXPackConfig } from '.'; + +export interface LegacySetup { + server: Server; +} + +export interface APMPluginContract { + config$: Observable; + registerLegacyAPI: (__LEGACY: LegacySetup) => void; +} + +export class APMPlugin implements Plugin { + legacySetup$: AsyncSubject; + constructor(private readonly initContext: PluginInitializerContext) { + this.initContext = initContext; + this.legacySetup$ = new AsyncSubject(); + } + + public setup( + core: CoreSetup, + plugins: { + apm_oss: APMOSSPlugin extends Plugin ? TSetup : never; + } + ) { + const config$ = this.initContext.config.create(); + const logger = this.initContext.logger.get('apm'); + + const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe( + map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) + ); + + this.legacySetup$.subscribe(__LEGACY => { + createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); + }); + + combineLatest(mergedConfig$, core.elasticsearch.dataClient$).subscribe( + ([config, dataClient]) => { + createApmAgentConfigurationIndex({ + esClient: dataClient, + config, + }); + } + ); + + return { + config$: mergedConfig$, + registerLegacyAPI: once((__LEGACY: LegacySetup) => { + this.legacySetup$.next(__LEGACY); + this.legacySetup$.complete(); + }), + }; + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json new file mode 100644 index 0000000000000..87214f0287054 --- /dev/null +++ b/x-pack/plugins/canvas/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "canvas", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": false, + "requiredPlugins": [] + } + \ No newline at end of file diff --git a/x-pack/plugins/canvas/server/index.ts b/x-pack/plugins/canvas/server/index.ts new file mode 100644 index 0000000000000..e881f7db69c78 --- /dev/null +++ b/x-pack/plugins/canvas/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { CanvasPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CanvasPlugin(initializerContext); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts new file mode 100644 index 0000000000000..76b86c2ac39b4 --- /dev/null +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server'; +import { initRoutes } from './routes'; + +export class CanvasPlugin implements Plugin { + private readonly logger: Logger; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(coreSetup: CoreSetup): void { + const canvasRouter = coreSetup.http.createRouter(); + + initRoutes({ router: canvasRouter, logger: this.logger }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts new file mode 100644 index 0000000000000..fb7f4d6ee2600 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.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 { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +export const catchErrorHandler: < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + fn: RequestHandler +) => RequestHandler = fn => { + return async (context, request, response) => { + try { + return await fn(context, request, response); + } catch (error) { + if (error.isBoom) { + return response.customError({ + body: error.output.payload, + statusCode: error.output.statusCode, + }); + } + return response.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts new file mode 100644 index 0000000000000..46873a6b32542 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger } from 'src/core/server'; +import { initWorkpadRoutes } from './workpad'; + +export interface RouteInitializerDeps { + router: IRouter; + logger: Logger; +} + +export function initRoutes(deps: RouteInitializerDeps) { + initWorkpadRoutes(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts new file mode 100644 index 0000000000000..dbad1a97dc458 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateWorkpadRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the workpad is created`, async () => { + const mockWorkpad = { + pages: [], + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: mockWorkpad, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...mockWorkpad, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `workpad-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts new file mode 100644 index 0000000000000..be904356720b6 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD}`, + validate: { + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + if (!request.body) { + return response.badRequest({ body: 'A workpad payload is required' }); + } + + const workpad = request.body as CanvasWorkpad; + + const now = new Date().toISOString(); + const { id, ...payload } = workpad; + + await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('workpad') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts new file mode 100644 index 0000000000000..e693840826b7a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteWorkpadRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the workpad is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts new file mode 100644 index 0000000000000..7adf11e7a887b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts new file mode 100644 index 0000000000000..08de9b20e9818 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { initializeFindWorkpadsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindWorkpadsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found workpads`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 2, + "workpads": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 0, + "workpads": Array [], + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.ts b/x-pack/plugins/canvas/server/routes/workpad/find.ts new file mode 100644 index 0000000000000..a528a75611609 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const workpads = await savedObjectsClient.find({ + type: CANVAS_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', '@created', '@timestamp'], + page, + perPage, + }); + + return response.ok({ + body: { + total: workpads.total, + workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + workpads: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts new file mode 100644 index 0000000000000..a31293f572c75 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetWorkpadRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-workpad", + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts new file mode 100644 index 0000000000000..7a51006aa9f02 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + request.params.id + ); + + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach(page => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } + + return response.ok({ + body: { + id: workpad.id, + ...workpad.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts new file mode 100644 index 0000000000000..8a61b30be5414 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeFindWorkpadsRoute } from './find'; +import { initializeGetWorkpadRoute } from './get'; +import { initializeCreateWorkpadRoute } from './create'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { initializeDeleteWorkpadRoute } from './delete'; + +export function initWorkpadRoutes(deps: RouteInitializerDeps) { + initializeFindWorkpadsRoute(deps); + initializeGetWorkpadRoute(deps); + initializeCreateWorkpadRoute(deps); + initializeUpdateWorkpadRoute(deps); + initializeUpdateWorkpadAssetsRoute(deps); + initializeDeleteWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts new file mode 100644 index 0000000000000..43d545a5183fe --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const okResponse = { + ok: true, +}; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts new file mode 100644 index 0000000000000..492a6c98d71ee --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 sinon from 'sinon'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { okResponse } from './ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const workpad = workpads[0]; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('PUT workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the workpad is updated`, async () => { + const updatedWorkpad = { name: 'new name' }; + const { id, ...workpadAttributes } = workpad; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + body: updatedWorkpad, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: workpadAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...workpadAttributes, + ...updatedWorkpad, + '@timestamp': nowIso, + '@created': workpad['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing workpad is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CANVAS_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); + +describe('update assets', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadAssetsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it('updates assets', async () => { + const { id, ...attributes } = workpad; + const assets = { + 'asset-1': { + '@created': new Date().toISOString(), + id: 'asset-1', + type: 'asset', + value: 'some-url-encoded-asset', + }, + 'asset-2': { + '@created': new Date().toISOString(), + id: 'asset-2', + type: 'asset', + value: 'some-other asset', + }, + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad-assets/some-id', + params: { + id, + }, + body: assets, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: attributes as any, + references: [], + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...attributes, + '@timestamp': nowIso, + assets, + }, + { + id, + overwrite: true, + } + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts new file mode 100644 index 0000000000000..460aa174038ae --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { KibanaResponseFactory } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_STRUCTURES, + API_ROUTE_WORKPAD_ASSETS, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); + +const AssetPayloadSchema = schema.object({ + assets: AssetsRecordSchema, +}); + +const workpadUpdateHandler = async ( + payload: TypeOf | TypeOf, + id: string, + savedObjectsClient: SavedObjectsClientContract, + response: KibanaResponseFactory +) => { + const now = new Date().toISOString(); + + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( + CANVAS_TYPE, + { + ...workpadObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); +}; + +export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + // TODO: This route is likely deprecated and everything is using the workpad_structures + // path instead. Investigate further. + router.put( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); + + router.put( + { + path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); +} + +export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + + router.put( + { + path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + // ToDo: Currently the validation must be a schema.object + // Because we don't know what keys the assets will have, we have to allow + // unknowns and then validate in the handler + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, response) => { + return workpadUpdateHandler( + { assets: AssetsRecordSchema.validate(request.body) }, + request.params.id, + context.core.savedObjects.client, + response + ); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts new file mode 100644 index 0000000000000..0bcb161575901 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const WorkpadElementSchema = schema.object({ + expression: schema.string(), + filter: schema.maybe(schema.nullable(schema.string())), + id: schema.string(), + position: PositionSchema, +}); + +export const WorkpadPageSchema = schema.object({ + elements: schema.arrayOf(WorkpadElementSchema), + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }) + ), + id: schema.string(), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const WorkpadAssetSchema = schema.object({ + '@created': schema.string(), + id: schema.string(), + type: schema.string(), + value: schema.string(), +}); + +export const WorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}); diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index 8ede881cad47e..60dfd1f6cb260 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -38,6 +38,25 @@ describe('licensing plugin', () => { expect(license.uid).toBe('fetched'); }); + + it('data re-fetch call marked as a system api', async () => { + const sessionStorage = coreMock.createStorage(); + plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); + + const coreSetup = coreMock.createSetup(); + const fetchedLicense = licenseMock.create(); + coreSetup.http.get.mockResolvedValue(fetchedLicense); + + const { refresh } = await plugin.setup(coreSetup); + + refresh(); + + expect(coreSetup.http.get.mock.calls[0][1]).toMatchObject({ + headers: { + 'kbn-system-api': 'true', + }, + }); + }); }); describe('#license$', () => { @@ -238,7 +257,7 @@ describe('licensing plugin', () => { }, }, request: { - url: 'http://10.10.10.10:5601/api/xpack/v1/info', + url: 'http://10.10.10.10:5601/api/licensing/info', }, }; expect(coreSetup.http.get).toHaveBeenCalledTimes(0); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index c1b13418aa3e7..79ad6f289b67e 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -5,7 +5,7 @@ */ import { Subject, Subscription, merge } from 'rxjs'; -import { takeUntil, tap } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; @@ -31,8 +31,9 @@ export class LicensingPlugin implements Plugin { */ private removeInterceptor?: () => void; private licenseFetchSubscription?: Subscription; + private storageSubscription?: Subscription; - private infoEndpoint = '/api/xpack/v1/info'; + private readonly infoEndpoint = '/api/licensing/info'; private prevSignature?: string; constructor( @@ -76,18 +77,16 @@ export class LicensingPlugin implements Plugin { ); this.licenseFetchSubscription = fetchSubscription; - const license$ = update$.pipe( - tap(license => { - if (license.error) { - this.prevSignature = undefined; - // Prevent reusing stale license if the fetch operation fails - this.removeSaved(); - } else { - this.prevSignature = license.signature; - this.save(license); - } - }) - ); + this.storageSubscription = update$.subscribe(license => { + if (license.isAvailable) { + this.prevSignature = license.signature; + this.save(license); + } else { + this.prevSignature = undefined; + // Prevent reusing stale license if the fetch operation fails + this.removeSaved(); + } + }); this.removeInterceptor = core.http.intercept({ response: async httpResponse => { @@ -107,7 +106,7 @@ export class LicensingPlugin implements Plugin { refresh: () => { manualRefresh$.next(); }, - license$, + license$: update$, }; } @@ -124,11 +123,20 @@ export class LicensingPlugin implements Plugin { this.licenseFetchSubscription.unsubscribe(); this.licenseFetchSubscription = undefined; } + if (this.storageSubscription !== undefined) { + this.storageSubscription.unsubscribe(); + this.storageSubscription = undefined; + } } private fetchLicense = async (core: CoreSetup): Promise => { try { - const response = await core.http.get(this.infoEndpoint); + const response = await core.http.get(this.infoEndpoint, { + headers: { + 'kbn-system-api': 'true', + }, + }); + return new License({ license: response.license, features: response.features, diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 3c93b55723787..d3dc84c05e25c 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -24,6 +24,8 @@ import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; import { ElasticsearchError, RawLicense, RawFeatures } from './types'; +import { registerRoutes } from './routes'; + import { LicenseConfigType } from './licensing_config'; import { createRouteHandlerContext } from './licensing_route_handler_context'; @@ -92,6 +94,7 @@ export class LicensingPlugin implements Plugin { const { refresh, license$ } = this.createLicensePoller(dataClient, config.pollingFrequency); core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$)); + registerRoutes(core.http.createRouter()); return { refresh, diff --git a/x-pack/plugins/licensing/server/routes/index.ts b/x-pack/plugins/licensing/server/routes/index.ts new file mode 100644 index 0000000000000..26b3bc6292dd6 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; +import { registerInfoRoute } from './info'; + +export function registerRoutes(router: IRouter) { + registerInfoRoute(router); +} diff --git a/x-pack/plugins/licensing/server/routes/info.ts b/x-pack/plugins/licensing/server/routes/info.ts new file mode 100644 index 0000000000000..cad873014e271 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/info.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; + +export function registerInfoRoute(router: IRouter) { + router.get({ path: '/api/licensing/info', validate: false }, (context, request, response) => { + return response.ok({ + body: context.licensing.license, + }); + }); +} diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts index 55d125bf993ec..7b1a554e1d3f1 100644 --- a/x-pack/plugins/security/public/plugin.ts +++ b/x-pack/plugins/security/public/plugin.ts @@ -13,6 +13,8 @@ import { } from './session'; export class SecurityPlugin implements Plugin { + private sessionTimeout!: SessionTimeout; + public setup(core: CoreSetup) { const { http, notifications, injectedMetadata } = core; const { basePath, anonymousPaths } = http; @@ -20,23 +22,25 @@ export class SecurityPlugin implements Plugin; diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 9c0e4cd8036cc..678c397dfbc64 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -7,40 +7,81 @@ import { coreMock } from 'src/core/public/mocks'; import { SessionExpired } from './session_expired'; -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -it('redirects user to "/logout" when there is no basePath', async () => { - const { basePath } = coreMock.createSetup().http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); +describe('Session Expiration', () => { + const mockGetItem = jest.fn().mockReturnValue(null); + + beforeAll(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: mockGetItem, + }, + writable: true, }); }); - sessionExpired.logout(); + afterAll(() => { + delete (window as any).sessionStorage; + }); + + describe('logout', () => { + const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); + const tenant = ''; - const url = await newUrlPromise; - expect(url).toBe( - `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); -}); + it('redirects user to "/logout" when there is no basePath', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); -it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { - const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); }); - }); - sessionExpired.logout(); + it('adds a provider parameter when an auth provider is saved in sessionStorage', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + mockGetItem.mockReturnValueOnce('basic'); + + sessionExpired.logout(); - const url = await newUrlPromise; - expect(url).toBe( - `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent( + '/foo/bar?baz=quz#quuz' + )}&msg=SESSION_EXPIRED&provider=basic` + ); + }); + + it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { + const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); + }); + }); }); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 3ef15088bb288..a43da85526757 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -11,14 +11,19 @@ export interface ISessionExpired { } export class SessionExpired { - constructor(private basePath: HttpSetup['basePath']) {} + constructor(private basePath: HttpSetup['basePath'], private tenant: string) {} logout() { const next = this.basePath.remove( `${window.location.pathname}${window.location.search}${window.location.hash}` ); + const key = `${this.tenant}/session_provider`; + const providerName = sessionStorage.getItem(key); + const provider = providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; window.location.assign( - this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`) + this.basePath.prepend( + `/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED${provider}` + ) ); } } diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx new file mode 100644 index 0000000000000..bb4116420f15d --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; + +describe('SessionIdleTimeoutWarning', () => { + it('fires its callback when the OK button is clicked', () => { + const handler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + expect(handler).toBeCalledTimes(0); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(handler).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx new file mode 100644 index 0000000000000..32e4dcc5c6b53 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiProgress } from '@elastic/eui'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + onRefreshSession: () => void; + timeout: number; +} + +export const SessionIdleTimeoutWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+
+ + + +
+ + ); +}; + +export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'warning', + text: toMountPoint( + + ), + title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'clock', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx new file mode 100644 index 0000000000000..7925e92bce4ed --- /dev/null +++ b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { EuiProgress } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + timeout: number; +} + +export const SessionLifespanWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+ + ); +}; + +export const createToast = (toastLifeTimeMs: number): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'danger', + text: toMountPoint(), + title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'alert', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 9917a50279083..df9b8628b180d 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -8,6 +8,8 @@ import { ISessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { + start: jest.fn(), + stop: jest.fn(), extend: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index 80a22c5fb0b2a..eb947ab95c43b 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import BroadcastChannel from 'broadcast-channel'; import { SessionTimeout } from './session_timeout'; import { createSessionExpiredMock } from './session_expired.mock'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -17,25 +18,46 @@ const expectNoWarningToast = ( expect(notifications.toasts.add).not.toHaveBeenCalled(); }; -const expectWarningToast = ( +const expectIdleTimeoutWarningToast = ( notifications: ReturnType['notifications'], - toastLifeTimeMS: number = 60000 + toastLifeTimeMs: number = 60000 ) => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "text": MountPoint { - "reactNode": , - }, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMS}, - }, - ] - `); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "warning", + "iconType": "clock", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); +}; + +const expectLifespanWarningToast = ( + notifications: ReturnType['notifications'], + toastLifeTimeMs: number = 60000 +) => { + expect(notifications.toasts.add).toHaveBeenCalledTimes(1); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "danger", + "iconType": "alert", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); }; const expectWarningToastHidden = ( @@ -46,128 +68,309 @@ const expectWarningToastHidden = ( expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); }; -describe('warning toast', () => { - test(`shows session expiration warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); +describe('Session Timeout', () => { + const now = new Date().getTime(); + const defaultSessionInfo = { + now, + idleTimeoutExpiration: now + 2 * 60 * 1000, + lifespanExpiration: null, + }; + let notifications: ReturnType['notifications']; + let http: ReturnType['http']; + let sessionExpired: ReturnType; + let sessionTimeout: SessionTimeout; + const toast = Symbol(); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + beforeAll(() => { + BroadcastChannel.enforceOptions({ + type: 'simulate', + }); + Object.defineProperty(window, 'sessionStorage', { + value: { + setItem: jest.fn(() => null), + }, + writable: true, + }); }); - test(`extend delays the warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + beforeEach(() => { + const setup = coreMock.createSetup(); + notifications = setup.notifications; + http = setup.http; + notifications.toasts.add.mockReturnValue(toast as any); + sessionExpired = createSessionExpiredMock(); + const tenant = ''; + sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + // default mocked response for checking session info + http.fetch.mockResolvedValue(defaultSessionInfo); + }); - jest.advanceTimersByTime(1 * 1000); + afterEach(async () => { + jest.clearAllMocks(); + }); - expectWarningToast(notifications); + afterAll(() => { + BroadcastChannel.enforceOptions(null); + delete (window as any).sessionStorage; }); - test(`extend hides displayed warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const toast = Symbol(); - notifications.toasts.add.mockReturnValue(toast as any); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('Lifecycle', () => { + test(`starts and initializes on a non-anonymous path`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).not.toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + test(`starts and does not initialize on an anonymous path`, async () => { + http.anonymousPaths.register(window.location.pathname); + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).not.toHaveBeenCalled(); + }); - sessionTimeout.extend(); - expectWarningToastHidden(notifications, toast); - }); + test(`stops`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + const close = jest.fn(sessionTimeout['channel']!.close); + // eslint-disable-next-line dot-notation + sessionTimeout['channel']!.close = close; + // eslint-disable-next-line dot-notation + const cleanup = jest.fn(sessionTimeout['cleanup']); + // eslint-disable-next-line dot-notation + sessionTimeout['cleanup'] = cleanup; - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); - - expect(http.get).not.toHaveBeenCalled(); - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.get).toHaveBeenCalled(); + sessionTimeout.stop(); + expect(close).toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalled(); + }); }); - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(64 * 1000, notifications, sessionExpired, http); + describe('API calls', () => { + const methodName = 'handleSessionInfoAndResetTimers'; + let method: jest.Mock; - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expectWarningToast(notifications, 59 * 1000); - }); -}); + beforeEach(() => { + method = jest.fn(sessionTimeout[methodName]); + sessionTimeout[methodName] = method; + }); -describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + test(`handles success`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); + expect(method).toHaveBeenCalledTimes(1); + }); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`handles error`, async () => { + const mockErrorResponse = new Error('some-error'); + http.fetch.mockRejectedValue(mockErrorResponse); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBeUndefined(); + expect(method).not.toHaveBeenCalled(); + }); }); - test(`extend delays the expiration`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('warning toast', () => { + test(`shows idle timeout warning toast`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectIdleTimeoutWarningToast(notifications); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + test(`shows lifespan warning toast`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); + }); + + test(`extend only results in an HTTP call if a warning is shown`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(54 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectNoWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(10 * 1000); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(4 * 1000, notifications, sessionExpired, http); + expect(http.fetch).toHaveBeenCalledTimes(1); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`extend hides displayed warning toast`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expectIdleTimeoutWarningToast(notifications); + + http.fetch.mockResolvedValue({ + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + expectWarningToastHidden(notifications, toast); + }); + + test(`extend does nothing for session-related routes`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/internal/security/session'); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test(`checks for updated session info before the warning displays`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we check for updated session info 1 second before the warning is shown + const elapsed = 54 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const toastInput = notifications.toasts.add.mock.calls[0][0]; + expect(toastInput).toHaveProperty('text'); + const mountPoint = (toastInput as any).text; + const wrapper = mountWithIntl(mountPoint.__reactMount__); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 64 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalled(); + + jest.advanceTimersByTime(0); + expectIdleTimeoutWarningToast(notifications, 59 * 1000); + }); }); - test(`'null' sessionTimeout never logs you out`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(null, notifications, sessionExpired, http); - sessionTimeout.extend(); - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + describe('session expiration', () => { + test(`expires the session 5 seconds before it really expires`, async () => { + await sessionTimeout.start(); + + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1 * 1000); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`extend delays the expiration`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + const elapsed = 114 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const sessionInfo = { + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); + + // at this point, the session is good for another 120 seconds + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated + // so we need an extra 100ms of padding for this test to ensure that logout has been called + jest.advanceTimersByTime(1 * 1000 + 100); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 4 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`'null' sessionTimeout never logs you out`, async () => { + http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(Number.MAX_VALUE); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index 32302effd6e46..0069e78b5f372 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsSetup, Toast, HttpSetup } from 'src/core/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; +import { BroadcastChannel } from 'broadcast-channel'; +import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; +import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; +import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -23,58 +23,188 @@ const GRACE_PERIOD_MS = 5 * 1000; */ const WARNING_MS = 60 * 1000; +/** + * Current session info is checked this number of milliseconds before the + * warning toast shows. This will prevent the toast from being shown if the + * session has already been extended. + */ +const SESSION_CHECK_MS = 1000; + +/** + * Route to get session info and extend session expiration + */ +const SESSION_ROUTE = '/internal/security/session'; + export interface ISessionTimeout { - extend(): void; + start(): void; + stop(): void; + extend(url: string): void; } export class SessionTimeout { - private warningTimeoutMilliseconds?: number; - private expirationTimeoutMilliseconds?: number; + private channel?: BroadcastChannel; + private sessionInfo?: SessionInfo; + private fetchTimer?: number; + private warningTimer?: number; + private expirationTimer?: number; private warningToast?: Toast; constructor( - private sessionTimeoutMilliseconds: number | null, private notifications: NotificationsSetup, private sessionExpired: ISessionExpired, - private http: HttpSetup + private http: HttpSetup, + private tenant: string ) {} - extend() { - if (this.sessionTimeoutMilliseconds == null) { + start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (this.warningTimeoutMilliseconds) { - window.clearTimeout(this.warningTimeoutMilliseconds); + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + } + + stop() { + if (this.channel) { + this.channel.close(); } - if (this.expirationTimeoutMilliseconds) { - window.clearTimeout(this.expirationTimeoutMilliseconds); + this.cleanup(); + } + + /** + * When the user makes an authenticated, non-system API call, this function is used to check + * and see if the session has been extended. + * @param url The URL that was called + */ + extend(url: string) { + // avoid an additional API calls when the user clicks the button on the session idle timeout + if (url.endsWith(SESSION_ROUTE)) { + return; } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); + + const { isLifespanTimeout } = this.getTimeout(); + if (this.warningToast && !isLifespanTimeout) { + // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; + // make a new call to get the latest session info + return this.fetchSessionInfoAndResetTimers(); + } + } + + /** + * Fetch latest session information from the server, and optionally attempt to extend + * the session expiration. + */ + private fetchSessionInfoAndResetTimers = async (extend = false) => { + const method = extend ? 'POST' : 'GET'; + const headers = extend ? {} : { 'kbn-system-api': 'true' }; + try { + const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + + this.handleSessionInfoAndResetTimers(result); + + // share this updated session info with any other tabs to sync the UX + if (this.channel) { + this.channel.postMessage(result); + } + } catch (err) { + // do nothing; 401 errors will be caught by the http interceptor + } + }; + + /** + * Processes latest session information, and resets timers based on it. These timers are + * used to trigger an HTTP call for updated session information, to show a timeout + * warning, and to log the user out when their session is expired. + */ + private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { + this.sessionInfo = sessionInfo; + // save the provider name in session storage, we will need it when we log out + const key = `${this.tenant}/session_provider`; + sessionStorage.setItem(key, sessionInfo.provider); + + const { timeout, isLifespanTimeout } = this.getTimeout(); + if (timeout == null) { + return; } - this.warningTimeoutMilliseconds = window.setTimeout( - () => this.showWarning(), - Math.max(this.sessionTimeoutMilliseconds - WARNING_MS - GRACE_PERIOD_MS, 0) + + this.cleanup(); + + // set timers + const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; + if (timeoutVal > 0 && !isLifespanTimeout) { + // we should check for the latest session info before the warning displays + this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal); + } + this.warningTimer = window.setTimeout( + this.showWarning, + Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) ); - this.expirationTimeoutMilliseconds = window.setTimeout( + this.expirationTimer = window.setTimeout( () => this.sessionExpired.logout(), - Math.max(this.sessionTimeoutMilliseconds - GRACE_PERIOD_MS, 0) + Math.max(timeout - GRACE_PERIOD_MS, 0) ); - } + }; - private showWarning = () => { - this.warningToast = this.notifications.toasts.add({ - color: 'warning', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - toastLifeTimeMs: Math.min(this.sessionTimeoutMilliseconds! - GRACE_PERIOD_MS, WARNING_MS), - }); + private cleanup = () => { + if (this.fetchTimer) { + window.clearTimeout(this.fetchTimer); + } + if (this.warningTimer) { + window.clearTimeout(this.warningTimer); + } + if (this.expirationTimer) { + window.clearTimeout(this.expirationTimer); + } + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + } }; - private refreshSession = () => { - this.http.get('/api/security/v1/me'); + /** + * Get the amount of time until the session times out, and whether or not the + * session has reached it maximum lifespan. + */ + private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { + let timeout = null; + let isLifespanTimeout = false; + if (this.sessionInfo) { + const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; + if (idleTimeoutExpiration) { + timeout = idleTimeoutExpiration - now; + } + if ( + lifespanExpiration && + (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) + ) { + timeout = lifespanExpiration - now; + isLifespanTimeout = true; + } + } + return { timeout, isLifespanTimeout }; + }; + + /** + * Show a warning toast depending on the session state. + */ + private showWarning = () => { + const { timeout, isLifespanTimeout } = this.getTimeout(); + const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); + let toast: ToastInput; + if (!isLifespanTimeout) { + const refresh = () => this.fetchSessionInfoAndResetTimers(true); + toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); + } else { + toast = createLifespanToast(toastLifeTimeMs); + } + this.warningToast = this.notifications.toasts.add(toast); }; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 98516cb4a613b..81625e1753b27 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -24,7 +24,7 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpResponse.request.url); } responseError(httpErrorResponse: HttpErrorResponse) { @@ -45,6 +45,6 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpErrorResponse.request.url); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx deleted file mode 100644 index a52e7ce4e94b5..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx +++ /dev/null @@ -1,19 +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 React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SessionTimeoutWarning } from './session_timeout_warning'; - -describe('SessionTimeoutWarning', () => { - it('fires its callback when the OK button is clicked', () => { - const handler = jest.fn(); - const wrapper = mountWithIntl(); - - expect(handler).toBeCalledTimes(0); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(handler).toBeCalledTimes(1); - }); -}); diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.tsx deleted file mode 100644 index e1b4542031ed1..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - onRefreshSession: () => void; -} - -export const SessionTimeoutWarning = (props: Props) => { - return ( - <> -

- -

-
- - - -
- - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c95..ff2db01cb6c58 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 0000000000000..e9c4b6e281cf3 --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 78c6feac0fa29..12b4620d554a2 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -73,7 +79,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -151,7 +159,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -173,7 +182,12 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -286,7 +300,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -344,7 +360,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -366,7 +383,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -381,7 +399,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -400,7 +423,58 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('properly extends session expiration if it is defined.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + + // Create new authenticator with non-null session `idleTimeout`. + mockOptions = getMockOptions({ + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -408,27 +482,39 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: currentDate + 3600 * 24, + lifespanExpiration: null, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + state, + provider: 'basic', + }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -445,13 +531,69 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); @@ -460,7 +602,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -477,7 +624,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -497,7 +649,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -508,7 +661,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -526,7 +680,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -537,7 +692,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -552,7 +708,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -569,7 +730,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -585,7 +751,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -602,7 +773,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -619,7 +795,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -636,7 +817,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -653,7 +839,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -668,7 +859,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -697,7 +890,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const deauthenticationResult = await authenticator.logout(request); @@ -707,10 +905,41 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const deauthenticationResult = await authenticator.logout(request); @@ -719,4 +948,51 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 18bdc9624b12b..17a773c6b6e8c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -77,7 +84,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -153,9 +160,14 @@ export class Authenticator { private readonly providers: Map; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -202,7 +214,9 @@ export class Authenticator { }) ); - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -257,10 +271,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } @@ -315,10 +331,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -334,6 +358,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -410,13 +457,34 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe01..77f1f9e45aea7 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index df16dd375e858..2e67a0eaaa6d5 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -68,15 +68,22 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => - !(sessionValue.expires && sessionValue.expires < Date.now()), + validate: (sessionValue: ProviderSession) => { + const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }, }), }); @@ -151,6 +158,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index 4ab47cb95b9a3..27105793fc966 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -574,7 +574,7 @@ describe('KerberosAuthenticationProvider', () => { sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 0e31dd3d51aba..767eab7b4311d 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -107,7 +107,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.failed(err); } - return DeauthenticationResult.redirectTo('/logged_out'); + return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index 7e00d8f282f62..c1d7dcca4c78f 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -598,7 +598,7 @@ describe('OIDCAuthenticationProvider', () => { }); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the OpenID Connect Provider if RP initiated SLO is supported.', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index 824189fa77a26..3737123645379 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -431,7 +431,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.redirectTo(redirect); } - return DeauthenticationResult.redirectTo(`${this.options.basePath.get(request)}/logged_out`); + return DeauthenticationResult.redirectTo( + `${this.options.basePath.serverBasePath}/logged_out` + ); } catch (err) { this.logger.debug(`Failed to deauthenticate user: ${err.message}`); return DeauthenticationResult.failed(err); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 35d827c3a9bd1..76442733e7368 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -583,7 +583,7 @@ describe('PKIAuthenticationProvider', () => { expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index fa3e1959ba7de..c7d431422a248 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -111,7 +111,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.failed(err); } - return DeauthenticationResult.redirectTo('/logged_out'); + return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 7ef1d934a7d13..27702f70865ea 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -1045,7 +1045,7 @@ describe('SAMLAuthenticationProvider', () => { }); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => { @@ -1069,7 +1069,7 @@ describe('SAMLAuthenticationProvider', () => { }); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML logout if query string is not empty, but does not include SAMLRequest.', async () => { @@ -1095,7 +1095,7 @@ describe('SAMLAuthenticationProvider', () => { }); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML invalidate call even if access token is presented.', async () => { @@ -1119,7 +1119,7 @@ describe('SAMLAuthenticationProvider', () => { ); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => { @@ -1139,7 +1139,7 @@ describe('SAMLAuthenticationProvider', () => { ); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => { @@ -1159,7 +1159,7 @@ describe('SAMLAuthenticationProvider', () => { ); expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/logged_out'); + expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index a8683796293af..faa19239fcc3b 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -228,7 +228,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.redirectTo(redirect); } - return DeauthenticationResult.redirectTo('/logged_out'); + return DeauthenticationResult.redirectTo( + `${this.options.basePath.serverBasePath}/logged_out` + ); } catch (err) { this.logger.debug(`Failed to deauthenticate user: ${err.message}`); return DeauthenticationResult.failed(err); diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2c..a6850dcdf8321 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498..c5f8f07e50b11 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 943d582bf484a..9ddb3e6e96b90 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -21,8 +21,12 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); @@ -35,8 +39,12 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); @@ -48,8 +56,12 @@ describe('config schema', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); }); @@ -250,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -270,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -290,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -310,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 6fe3fc73e458c..c7d990f81369e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -26,6 +26,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type) = export const ConfigSchema = schema.object( { + loginAssistanceMessage: schema.string({ defaultValue: '' }), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), @@ -33,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -82,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index b0e2ae7176834..26788c3ef9230 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -52,8 +55,12 @@ describe('Security Plugin', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -65,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4b3997fe74f1b..e956603517349 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -205,7 +208,11 @@ export class Plugin { // We should stop exposing this config as soon as only new platform plugin consumes it. The only // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { - sessionTimeout: config.sessionTimeout, + loginAssistanceMessage: config.loginAssistanceMessage, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb9..086647dcb3459 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 0000000000000..cdebc19d7cf8d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 313e4415a8e7c..d806aaf1807ef 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], "requiredPlugins": ["features", "licensing"], - "optionalPlugins": ["security", "home"], + "optionalPlugins": ["security", "home", "usageCollection"], "server": true, "ui": false } diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts deleted file mode 100644 index 912cccbc01782..0000000000000 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts +++ /dev/null @@ -1,170 +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 { getSpacesUsageCollector, UsageStats } from './get_spaces_usage_collector'; -import * as Rx from 'rxjs'; -import { PluginsSetup } from '../plugin'; -import { Feature } from '../../../features/server'; -import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; - -interface SetupOpts { - license?: Partial; - features?: Feature[]; -} - -function setup({ - license = { isAvailable: true }, - features = [{ id: 'feature1' } as Feature, { id: 'feature2' } as Feature], -}: SetupOpts = {}) { - class MockUsageCollector { - private fetch: any; - - constructor({ fetch }: any) { - this.fetch = fetch; - } - // to make typescript happy - public fakeFetchUsage() { - return this.fetch; - } - } - - const licensing = { - license$: Rx.of(license), - } as LicensingPluginSetup; - - const featuresSetup = ({ - getFeatures: jest.fn().mockReturnValue(features), - } as unknown) as PluginsSetup['features']; - - return { - licensing, - features: featuresSetup, - usage: { - collectorSet: { - makeUsageCollector: (options: any) => new MockUsageCollector(options), - }, - }, - }; -} - -const defaultCallClusterMock = jest.fn().mockResolvedValue({ - hits: { - total: { - value: 2, - }, - }, - aggregations: { - disabledFeatures: { - buckets: [ - { - key: 'feature1', - doc_count: 1, - }, - ], - }, - }, -}); - -describe('with a basic license', () => { - let usageStats: UsageStats; - beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: true, type: 'basic' } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ - kibanaIndex: '.kibana', - usage, - features, - licensing, - }); - usageStats = await getSpacesUsage(defaultCallClusterMock); - }); - - test('sets enabled to true', () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets available to true', () => { - expect(usageStats.available).toBe(true); - }); - - test('sets the number of spaces', () => { - expect(usageStats.count).toBe(2); - }); - - test('calculates feature control usage', () => { - expect(usageStats.usesFeatureControls).toBe(true); - expect(usageStats).toHaveProperty('disabledFeatures'); - expect(usageStats.disabledFeatures).toEqual({ - feature1: 1, - feature2: 0, - }); - }); -}); - -describe('with no license', () => { - let usageStats: UsageStats; - beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: false } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ - kibanaIndex: '.kibana', - usage, - features, - licensing, - }); - usageStats = await getSpacesUsage(defaultCallClusterMock); - }); - - test('sets enabled to false', () => { - expect(usageStats.enabled).toBe(false); - }); - - test('sets available to false', () => { - expect(usageStats.available).toBe(false); - }); - - test('does not set the number of spaces', () => { - expect(usageStats.count).toBeUndefined(); - }); - - test('does not set feature control usage', () => { - expect(usageStats.usesFeatureControls).toBeUndefined(); - }); -}); - -describe('with platinum license', () => { - let usageStats: UsageStats; - beforeAll(async () => { - const { features, licensing, usage } = setup({ - license: { isAvailable: true, type: 'platinum' }, - }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ - kibanaIndex: '.kibana', - usage, - features, - licensing, - }); - usageStats = await getSpacesUsage(defaultCallClusterMock); - }); - - test('sets enabled to true', () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets available to true', () => { - expect(usageStats.available).toBe(true); - }); - - test('sets the number of spaces', () => { - expect(usageStats.count).toBe(2); - }); - - test('calculates feature control usage', () => { - expect(usageStats.usesFeatureControls).toBe(true); - expect(usageStats.disabledFeatures).toEqual({ - feature1: 1, - feature2: 0, - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts b/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts deleted file mode 100644 index bfbc5e6ab775d..0000000000000 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { CallAPIOptions } from 'src/core/server'; -import { take } from 'rxjs/operators'; -// @ts-ignore -import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; -import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; -import { PluginsSetup } from '../plugin'; - -type CallCluster = ( - endpoint: string, - clientParams: Record, - options?: CallAPIOptions -) => Promise; - -interface SpacesAggregationResponse { - hits: { - total: { value: number }; - }; - aggregations: { - [aggName: string]: { - buckets: Array<{ key: string; doc_count: number }>; - }; - }; -} - -/** - * - * @param {CallCluster} callCluster - * @param {string} kibanaIndex - * @param {PluginsSetup['features']} features - * @param {boolean} spacesAvailable - * @return {UsageStats} - */ -async function getSpacesUsage( - callCluster: CallCluster, - kibanaIndex: string, - features: PluginsSetup['features'], - spacesAvailable: boolean -) { - if (!spacesAvailable) { - return {} as UsageStats; - } - - const knownFeatureIds = features.getFeatures().map(feature => feature.id); - - const resp = await callCluster('search', { - index: kibanaIndex, - body: { - track_total_hits: true, - query: { - term: { - type: { - value: 'space', - }, - }, - }, - aggs: { - disabledFeatures: { - terms: { - field: 'space.disabledFeatures', - include: knownFeatureIds, - size: knownFeatureIds.length, - }, - }, - }, - size: 0, - }, - }); - - const { hits, aggregations } = resp; - - const count = get(hits, 'total.value', 0); - const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []); - - const initialCounts = knownFeatureIds.reduce( - (acc, featureId) => ({ ...acc, [featureId]: 0 }), - {} - ); - - const disabledFeatures: Record = disabledFeatureBuckets.reduce( - (acc, { key, doc_count }) => { - return { - ...acc, - [key]: doc_count, - }; - }, - initialCounts - ); - - const usesFeatureControls = Object.values(disabledFeatures).some( - disabledSpaceCount => disabledSpaceCount > 0 - ); - - return { - count, - usesFeatureControls, - disabledFeatures, - } as UsageStats; -} - -export interface UsageStats { - available: boolean; - enabled: boolean; - count?: number; - usesFeatureControls?: boolean; - disabledFeatures?: { - [featureId: string]: number; - }; -} - -interface CollectorDeps { - kibanaIndex: string; - usage: { collectorSet: any }; - features: PluginsSetup['features']; - licensing: PluginsSetup['licensing']; -} - -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function getSpacesUsageCollector(deps: CollectorDeps) { - const { collectorSet } = deps.usage; - return collectorSet.makeUsageCollector({ - type: KIBANA_SPACES_STATS_TYPE, - isReady: () => true, - fetch: async (callCluster: CallCluster) => { - const license = await deps.licensing.license$.pipe(take(1)).toPromise(); - const available = license.isAvailable; // some form of spaces is available for all valid licenses - - const usageStats = await getSpacesUsage( - callCluster, - deps.kibanaIndex, - deps.features, - available - ); - - return { - available, - enabled: available, - ...usageStats, - } as UsageStats; - }, - - /* - * Format the response data into a model for internal upload - * 1. Make this data part of the "kibana_stats" type - * 2. Organize the payload in the usage.xpack.spaces namespace of the data payload - */ - formatForBulkUpload: (result: UsageStats) => { - return { - type: KIBANA_STATS_TYPE_MONITORING, - payload: { - usage: { - spaces: result, - }, - }, - }; - }, - }); -} diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts new file mode 100644 index 0000000000000..b343bac9343a3 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; +import * as Rx from 'rxjs'; +import { PluginsSetup } from '../plugin'; +import { Feature } from '../../../features/server'; +import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; + +interface SetupOpts { + license?: Partial; + features?: Feature[]; +} + +function setup({ + license = { isAvailable: true }, + features = [{ id: 'feature1' } as Feature, { id: 'feature2' } as Feature], +}: SetupOpts = {}) { + class MockUsageCollector { + private fetch: any; + + constructor({ fetch }: any) { + this.fetch = fetch; + } + // to make typescript happy + public fakeFetchUsage() { + return this.fetch; + } + } + + const licensing = { + license$: Rx.of(license), + } as LicensingPluginSetup; + + const featuresSetup = ({ + getFeatures: jest.fn().mockReturnValue(features), + } as unknown) as PluginsSetup['features']; + + return { + licensing, + features: featuresSetup, + usageCollecion: { + makeUsageCollector: (options: any) => new MockUsageCollector(options), + }, + }; +} + +const defaultCallClusterMock = jest.fn().mockResolvedValue({ + hits: { + total: { + value: 2, + }, + }, + aggregations: { + disabledFeatures: { + buckets: [ + { + key: 'feature1', + doc_count: 1, + }, + ], + }, + }, +}); + +describe('with a basic license', () => { + let usageStats: UsageStats; + beforeAll(async () => { + const { features, licensing, usageCollecion } = setup({ + license: { isAvailable: true, type: 'basic' }, + }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + kibanaIndex: '.kibana', + features, + licensing, + }); + usageStats = await getSpacesUsage(defaultCallClusterMock); + }); + + test('sets enabled to true', () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets available to true', () => { + expect(usageStats.available).toBe(true); + }); + + test('sets the number of spaces', () => { + expect(usageStats.count).toBe(2); + }); + + test('calculates feature control usage', () => { + expect(usageStats.usesFeatureControls).toBe(true); + expect(usageStats).toHaveProperty('disabledFeatures'); + expect(usageStats.disabledFeatures).toEqual({ + feature1: 1, + feature2: 0, + }); + }); +}); + +describe('with no license', () => { + let usageStats: UsageStats; + beforeAll(async () => { + const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + kibanaIndex: '.kibana', + features, + licensing, + }); + usageStats = await getSpacesUsage(defaultCallClusterMock); + }); + + test('sets enabled to false', () => { + expect(usageStats.enabled).toBe(false); + }); + + test('sets available to false', () => { + expect(usageStats.available).toBe(false); + }); + + test('does not set the number of spaces', () => { + expect(usageStats.count).toBeUndefined(); + }); + + test('does not set feature control usage', () => { + expect(usageStats.usesFeatureControls).toBeUndefined(); + }); +}); + +describe('with platinum license', () => { + let usageStats: UsageStats; + beforeAll(async () => { + const { features, licensing, usageCollecion } = setup({ + license: { isAvailable: true, type: 'platinum' }, + }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + kibanaIndex: '.kibana', + features, + licensing, + }); + usageStats = await getSpacesUsage(defaultCallClusterMock); + }); + + test('sets enabled to true', () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets available to true', () => { + expect(usageStats.available).toBe(true); + }); + + test('sets the number of spaces', () => { + expect(usageStats.count).toBe(2); + }); + + test('calculates feature control usage', () => { + expect(usageStats.usesFeatureControls).toBe(true); + expect(usageStats.disabledFeatures).toEqual({ + feature1: 1, + feature2: 0, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts new file mode 100644 index 0000000000000..eb6843cfe4538 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { CallAPIOptions } from 'src/core/server'; +import { take } from 'rxjs/operators'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +// @ts-ignore +import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; +import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; +import { PluginsSetup } from '../plugin'; + +type CallCluster = ( + endpoint: string, + clientParams: Record, + options?: CallAPIOptions +) => Promise; + +interface SpacesAggregationResponse { + hits: { + total: { value: number }; + }; + aggregations: { + [aggName: string]: { + buckets: Array<{ key: string; doc_count: number }>; + }; + }; +} + +/** + * + * @param {CallCluster} callCluster + * @param {string} kibanaIndex + * @param {PluginsSetup['features']} features + * @param {boolean} spacesAvailable + * @return {UsageStats} + */ +async function getSpacesUsage( + callCluster: CallCluster, + kibanaIndex: string, + features: PluginsSetup['features'], + spacesAvailable: boolean +) { + if (!spacesAvailable) { + return {} as UsageStats; + } + + const knownFeatureIds = features.getFeatures().map(feature => feature.id); + + const resp = await callCluster('search', { + index: kibanaIndex, + body: { + track_total_hits: true, + query: { + term: { + type: { + value: 'space', + }, + }, + }, + aggs: { + disabledFeatures: { + terms: { + field: 'space.disabledFeatures', + include: knownFeatureIds, + size: knownFeatureIds.length, + }, + }, + }, + size: 0, + }, + }); + + const { hits, aggregations } = resp; + + const count = get(hits, 'total.value', 0); + const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []); + + const initialCounts = knownFeatureIds.reduce( + (acc, featureId) => ({ ...acc, [featureId]: 0 }), + {} + ); + + const disabledFeatures: Record = disabledFeatureBuckets.reduce( + (acc, { key, doc_count }) => { + return { + ...acc, + [key]: doc_count, + }; + }, + initialCounts + ); + + const usesFeatureControls = Object.values(disabledFeatures).some( + disabledSpaceCount => disabledSpaceCount > 0 + ); + + return { + count, + usesFeatureControls, + disabledFeatures, + } as UsageStats; +} + +export interface UsageStats { + available: boolean; + enabled: boolean; + count?: number; + usesFeatureControls?: boolean; + disabledFeatures?: { + [featureId: string]: number; + }; +} + +interface CollectorDeps { + kibanaIndex: string; + features: PluginsSetup['features']; + licensing: PluginsSetup['licensing']; +} + +/* + * @param {Object} server + * @return {Object} kibana usage stats type collection object + */ +export function getSpacesUsageCollector( + usageCollection: UsageCollectionSetup, + deps: CollectorDeps +) { + return usageCollection.makeUsageCollector({ + type: KIBANA_SPACES_STATS_TYPE, + isReady: () => true, + fetch: async (callCluster: CallCluster) => { + const license = await deps.licensing.license$.pipe(take(1)).toPromise(); + const available = license.isAvailable; // some form of spaces is available for all valid licenses + + const usageStats = await getSpacesUsage( + callCluster, + deps.kibanaIndex, + deps.features, + available + ); + + return { + available, + enabled: available, + ...usageStats, + } as UsageStats; + }, + + /* + * Format the response data into a model for internal upload + * 1. Make this data part of the "kibana_stats" type + * 2. Organize the payload in the usage.xpack.spaces namespace of the data payload + */ + formatForBulkUpload: (result: UsageStats) => { + return { + type: KIBANA_STATS_TYPE_MONITORING, + payload: { + usage: { + spaces: result, + }, + }, + }; + }, + }); +} + +export function registerSpacesUsageCollector( + usageCollection: UsageCollectionSetup | undefined, + deps: CollectorDeps +) { + if (!usageCollection) { + return; + } + const collector = getSpacesUsageCollector(usageCollection, deps); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 21120ab37b06a..9d45dbb1b748d 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -7,6 +7,8 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { CapabilitiesModifier } from 'src/legacy/server/capabilities'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { HomeServerPluginSetup } from 'src/plugins/home/server'; import { SavedObjectsLegacyService, CoreSetup, @@ -24,25 +26,19 @@ import { AuditLogger } from '../../../../server/lib/audit_logger'; import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; -import { getSpacesUsageCollector } from './lib/get_spaces_usage_collector'; +import { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; import { SpacesService } from './spaces_service'; import { SpacesServiceSetup } from './spaces_service/spaces_service'; import { ConfigType } from './config'; import { toggleUICapabilities } from './lib/toggle_ui_capabilities'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; import { initExternalSpacesApi } from './routes/api/external'; -import { HomePluginSetup } from '../../../../src/plugins/home/server'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin * to function properly. */ export interface LegacyAPI { savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - register: (collector: any) => void; - }; - }; tutorial: { addScopedTutorialContextFactory: (factory: any) => void; }; @@ -62,7 +58,8 @@ export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; security?: SecurityPluginSetup; - home?: HomePluginSetup; + usageCollection?: UsageCollectionSetup; + home?: HomeServerPluginSetup; } export interface SpacesPluginSetup { @@ -150,7 +147,12 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents(spacesService, plugins.features, plugins.licensing); + this.setupLegacyComponents( + spacesService, + plugins.features, + plugins.licensing, + plugins.usageCollection + ); }, createDefaultSpace: async () => { const esClient = await core.elasticsearch.adminClient$.pipe(take(1)).toPromise(); @@ -168,7 +170,8 @@ export class Plugin { private setupLegacyComponents( spacesService: SpacesServiceSetup, featuresSetup: FeaturesPluginSetup, - licensingSetup: LicensingPluginSetup + licensingSetup: LicensingPluginSetup, + usageCollectionSetup?: UsageCollectionSetup ) { const legacyAPI = this.getLegacyAPI(); const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; @@ -180,6 +183,12 @@ export class Plugin { legacyAPI.tutorial.addScopedTutorialContextFactory( createSpacesTutorialContextFactory(spacesService) ); + // Register a function with server to manage the collection of usage stats + registerSpacesUsageCollector(usageCollectionSetup, { + kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, + features: featuresSetup, + licensing: licensingSetup, + }); legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => { try { const activeSpace = await spacesService.getActiveSpace(KibanaRequest.from(request)); @@ -189,14 +198,5 @@ export class Plugin { return uiCapabilities; } }); - // Register a function with server to manage the collection of usage stats - legacyAPI.usage.collectorSet.register( - getSpacesUsageCollector({ - kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, - usage: legacyAPI.usage, - features: featuresSetup, - licensing: licensingSetup, - }) - ); } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 38a973c1203d5..62820466b571c 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -106,7 +106,6 @@ export const createLegacyAPI = ({ auditLogger: {} as any, capabilities: {} as any, tutorial: {} as any, - usage: {} as any, xpackMain: {} as any, savedObjects: savedObjectsService, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b59036b89f49..217b20797492a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -616,8 +616,6 @@ "core.euiSelectable.noAvailableOptions": "利用可能なオプションがありません", "core.euiSelectable.noMatchingOptions": "{searchValue} はどのオプションにも一致していません", "core.euiStat.loadingText": "統計を読み込み中です", - "core.euiStep.completeStep": "手順", - "core.euiStep.incompleteStep": "未完了の手順", "core.euiStepHorizontal.buttonTitle": "ステップ {step}: {title}{titleAppendix, select, completed { が完了} 無効 { が無効} other {}}", "core.euiStepHorizontal.step": "手順", "core.euiStepNumber.hasErrors": "エラーがあります", @@ -872,6 +870,9 @@ "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", + "devTools.badge.readOnly.text": "読み込み専用", + "devTools.badge.readOnly.tooltip": "を保存できませんでした", + "devTools.k7BreadcrumbsDevToolsLabel": "開発ツール", "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", "data.filter.filterEditor.rangeInputLabel": "範囲", @@ -879,6 +880,9 @@ "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", "data.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", + "data.functions.esaggs.help": "AggConfig 集約を実行します", + "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", + "data.functions.esaggs.inspector.dataRequest.title": "データ", "embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用", "embeddableApi.addPanel.createNewDefaultOption": "新規作成...", "embeddableApi.addPanel.displayName": "パネルの追加", @@ -954,9 +958,6 @@ "visualizations.function.visDimension.accessor.help": "使用するデータセット内の列 (列インデックスまたは列名)", "visualizations.function.visDimension.error.accessor": "入力された列名は無効です。", "visualizations.function.visDimension.help": "visConfig ディメンションオブジェクトを生成します", - "data.functions.esaggs.help": "AggConfig 集約を実行します", - "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", - "data.functions.esaggs.inspector.dataRequest.title": "データ", "expressions.functions.kibana_context.help": "Kibana グローバルコンテキストを更新します", "expressions.functions.kibana.help": "Kibana グローバルコンテキストを取得します", "expressions.functions.font.args.alignHelpText": "水平テキスト配置", @@ -1440,7 +1441,6 @@ "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", "kbn.dashboard.addVisualizationDescription1": "上のメニューバーの ", "kbn.dashboard.addVisualizationDescription2": " ボタンをクリックして、ダッシュボードにビジュアライゼーションを追加します。", - "kbn.dashboard.addVisualizationDescription3": "まだビジュアライゼーションをセットアップしていない場合は、{visitVisualizeAppLink} して初めのビジュアライゼーションを作成します。", "kbn.dashboard.addVisualizationLinkAriaLabel": "ビジュアライゼーションを追加", "kbn.dashboard.addVisualizationLinkText": "追加", "kbn.dashboard.badge.readOnly.text": "読み込み専用", @@ -1508,9 +1508,6 @@ "kbn.dashboard.urlWasRemovedInSixZeroWarningMessage": "URL「dashboard/create」は 6.0 で廃止されました。ブックマークを更新してください。", "kbn.dashboard.visitVisualizeAppLinkText": "可視化アプリにアクセス", "kbn.dashboardTitle": "ダッシュボード", - "kbn.devTools.badge.readOnly.text": "読み込み専用", - "kbn.devTools.badge.readOnly.tooltip": "を保存できませんでした", - "kbn.devTools.k7BreadcrumbsDevToolsLabel": "開発ツール", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", "kbn.discover.badge.readOnly.text": "読み込み専用", @@ -2530,12 +2527,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "フィールド詳細を切り替える", - "kbnESQuery.kql.errors.endOfInputText": "インプットの終わり", - "kbnESQuery.kql.errors.fieldNameText": "フィールド名", - "kbnESQuery.kql.errors.literalText": "文字通り", - "kbnESQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", - "kbnESQuery.kql.errors.valueText": "値", - "kbnESQuery.kql.errors.whitespaceText": "ホワイトスペース", + "data.common.esQuery.kql.errors.endOfInputText": "インプットの終わり", + "data.common.esQuery.kql.errors.fieldNameText": "フィールド名", + "data.common.esQuery.kql.errors.literalText": "文字通り", + "data.common.esQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", + "data.common.esQuery.kql.errors.valueText": "値", + "data.common.esQuery.kql.errors.whitespaceText": "ホワイトスペース", "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", "kbnVislibVisTypes.area.areaTitle": "エリア", "kbnVislibVisTypes.area.groupTitle": "系列を分割", @@ -3359,7 +3356,6 @@ "xpack.apm.errorsTable.occurrencesColumnLabel": "オカレンス", "xpack.apm.errorsTable.unhandledLabel": "未対応", "xpack.apm.featureRegistry.apmFeatureName": "APM", - "xpack.apm.feedbackMenu.provideFeedbackTitle": "APM のフィードバックを提供", "xpack.apm.filter.environment.allLabel": "すべて", "xpack.apm.filter.environment.label": "環境", "xpack.apm.filter.environment.notDefinedLabel": "未定義", @@ -5845,7 +5841,6 @@ "xpack.infra.homePage.noMetricsIndicesTitle": "メトリックインデックスがないようです。", "xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "インフラストラクチャーデータを検索… (例: host.name:host-1)", "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "指定期間の最後の 1 分間のデータを表示中", - "xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText": "インフラストラクチャーのフィードバックを提供", "xpack.infra.infrastructureDescription": "インフラストラクチャーを閲覧します", "xpack.infra.infrastructureMetricsExplorerPage.documentTitle": "{previousTitle} | メトリックエクスプローラー", "xpack.infra.infrastructureSnapshotPage.documentTitle": "{previousTitle} | インベントリ", @@ -5896,7 +5891,6 @@ "xpack.infra.logs.stopStreamingButtonLabel": "ストリーム停止", "xpack.infra.logs.streamingDescription": "新しいエントリーをストリーム中...", "xpack.infra.logs.streamingNewEntriesText": "新しいエントリーをストリーム中", - "xpack.infra.logsPage.logsHelpContent.feedbackLinkText": "ログのフィードバックを提供", "xpack.infra.logsPage.noLoggingIndicesDescription": "追加しましょう!", "xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "セットアップの手順を表示", "xpack.infra.logsPage.noLoggingIndicesTitle": "ログインデックスがないようです。", @@ -6420,8 +6414,6 @@ "xpack.maps.heatmap.colorRampLabel": "色の範囲", "xpack.maps.heatmapLegend.coldLabel": "コールド", "xpack.maps.heatmapLegend.hotLabel": "ホット", - "xpack.maps.helpMenu.docLabel": "Maps ドキュメンテーション", - "xpack.maps.helpMenu.feedbackLinkText": "Maps アプリケーションに関するフィードバックを提供", "xpack.maps.inspector.centerLatLabel": "中央緯度", "xpack.maps.inspector.centerLonLabel": "中央経度", "xpack.maps.inspector.mapboxStyleTitle": "マップボックススタイル", @@ -6621,7 +6613,6 @@ "xpack.maps.source.wmsTitle": "ウェブマップサービス", "xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}", - "xpack.maps.style.vector.displayNameLabel": "ベクタースタイル", "xpack.maps.styles.staticDynamic.dynamicDescription": "プロパティ値で特徴をシンボル化します。", "xpack.maps.styles.staticDynamic.staticDescription": "静的スタイルプロパティで特徴をシンボル化します。", "xpack.maps.styles.vector.borderColorLabel": "境界線の色", @@ -9938,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", @@ -10367,8 +10357,6 @@ "xpack.siem.certificate.fingerprint.clientCertLabel": "クライアント証明書", "xpack.siem.certificate.fingerprint.serverCertLabel": "サーバー証明書", "xpack.siem.chart.dataNotAvailableTitle": "チャートデータが利用できません", - "xpack.siem.chrome.help.feedback": "フィードバックを送信", - "xpack.siem.chrome.help.title": "SIEM アプリケーションのヘルプ", "xpack.siem.clipboard.copied": "コピー完了", "xpack.siem.clipboard.copy": "コピー", "xpack.siem.clipboard.to.the.clipboard": "クリップボードに", @@ -10604,7 +10592,7 @@ "xpack.siem.overview.feedbackTitle": "フィードバック", "xpack.siem.overview.filebeatCiscoTitle": "Filebeat Cisco", "xpack.siem.overview.filebeatNetflowTitle": "Filebeat Netflow", - "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Network", + "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Networks", "xpack.siem.overview.fileBeatSuricataTitle": "Filebeat Suricata", "xpack.siem.overview.filebeatSystemModuleTitle": "Filebeat システムモジュール", "xpack.siem.overview.fileBeatZeekTitle": "Filebeat Zeek", @@ -11799,8 +11787,6 @@ "xpack.uptime.filterBar.options.location.name": "場所", "xpack.uptime.filterBar.options.portLabel": "ポート", "xpack.uptime.filterBar.options.schemeLabel": "スキーム", - "xpack.uptime.header.helpLinkAriaLabel": "ディスカッションページへ移動", - "xpack.uptime.header.helpLinkText": "アップタイムのフィードバックを提供", "xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。", "xpack.uptime.monitorCharts.checkStatus.series.downCountLabel": "ダウンカウント", "xpack.uptime.monitorCharts.checkStatus.series.upCountLabel": "アップカウント", @@ -11862,8 +11848,6 @@ "xpack.uptime.snapshotHistogram.series.upLabel": "アップ", "xpack.uptime.uptimeFeatureCatalogueTitle": "起動時間", "xpack.uptime.emptyState.noDataMessage": "アップタイムデータが見つかりませんでした", - "xpack.uptime.header.docsLinkAriaLabel": "アップタイムドキュメンテーションに移動", - "xpack.uptime.header.documentationLinkText": "アップタイムドキュメンテーション", "xpack.uptime.pingList.collapseRow": "縮小", "xpack.uptime.pingList.durationMsColumnFormatting": "{millis}ミリ秒", "xpack.uptime.pingList.expandedRow.bodySize": "本文サイズは {bodyBytes} です。", @@ -12666,8 +12650,6 @@ "xpack.lens.functions.mergeTables.help": "いくつかの Kibana 表を 1 つの表に結合するのをアシストします", "xpack.lens.functions.renameColumns.help": "データベースの列の名前の変更をアシストします", "xpack.lens.functions.renameColumns.idMap.help": "キーが古い列 ID で値が対応する新しい列 ID となるように JSON エンコーディングされたオブジェクトです。他の列 ID はすべてのそのままです。", - "xpack.lens.helpMenu.docLabel": "レンズドキュメンテーション", - "xpack.lens.helpMenu.feedbackLinkText": "レンズアプリケーションに関するフィードバックを提供", "xpack.lens.indexPattern.avg": "平均", "xpack.lens.indexPattern.avgOf": "{name} の平均", "xpack.lens.indexPattern.cardinality": "ユニークカウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2b3c788125e6a..6a2ba20af7714 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -617,8 +617,6 @@ "core.euiSelectable.noAvailableOptions": "没有任何可用选项", "core.euiSelectable.noMatchingOptions": "{searchValue} 不匹配任何选项", "core.euiStat.loadingText": "统计正在加载", - "core.euiStep.completeStep": "步骤", - "core.euiStep.incompleteStep": "未完成步骤", "core.euiStepHorizontal.buttonTitle": "第 {step} 步:{title}{titleAppendix, select, completed {已完成} disabled {已禁用} other {}}", "core.euiStepHorizontal.step": "步骤", "core.euiStepNumber.hasErrors": "有错误", @@ -873,6 +871,9 @@ "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", + "devTools.badge.readOnly.text": "只读", + "devTools.badge.readOnly.tooltip": "无法保存", + "devTools.k7BreadcrumbsDevToolsLabel": "开发工具", "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", "data.filter.filterEditor.rangeInputLabel": "范围", @@ -880,6 +881,9 @@ "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", "data.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", + "data.functions.esaggs.help": "运行 AggConfig 聚合", + "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", + "data.functions.esaggs.inspector.dataRequest.title": "数据", "embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图", "embeddableApi.addPanel.createNewDefaultOption": "创建新的......", "embeddableApi.addPanel.displayName": "添加面板", @@ -955,9 +959,6 @@ "visualizations.function.visDimension.accessor.help": "数据集中要使用的列(列索引或列名称)", "visualizations.function.visDimension.error.accessor": "提供的列名称无效", "visualizations.function.visDimension.help": "生成 visConfig 维度对象", - "data.functions.esaggs.help": "运行 AggConfig 聚合", - "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", - "data.functions.esaggs.inspector.dataRequest.title": "数据", "expressions.functions.kibana_context.help": "更新 kibana 全局上下文", "expressions.functions.kibana.help": "获取 kibana 全局上下文", "expressions.functions.font.args.alignHelpText": "水平文本对齐。", @@ -1441,7 +1442,6 @@ "kbn.context.unableToLoadDocumentDescription": "无法加载文档", "kbn.dashboard.addVisualizationDescription1": "单击上述菜单栏中的 ", "kbn.dashboard.addVisualizationDescription2": " 按钮,以将可视化添加到仪表板。", - "kbn.dashboard.addVisualizationDescription3": "如果尚未设置任何可视化,请{visitVisualizeAppLink}以创建您的第一个可视化。", "kbn.dashboard.addVisualizationLinkAriaLabel": "添加可视化", "kbn.dashboard.addVisualizationLinkText": "添加", "kbn.dashboard.badge.readOnly.text": "只读", @@ -1509,9 +1509,6 @@ "kbn.dashboard.urlWasRemovedInSixZeroWarningMessage": "6.0 中未移除 url“dashboard/create”。请更新您的书签。", "kbn.dashboard.visitVisualizeAppLinkText": "访问 Visualize 应用", "kbn.dashboardTitle": "仪表板", - "kbn.devTools.badge.readOnly.text": "只读", - "kbn.devTools.badge.readOnly.tooltip": "无法保存", - "kbn.devTools.k7BreadcrumbsDevToolsLabel": "开发工具", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", "kbn.discover.badge.readOnly.text": "只读", @@ -2531,12 +2528,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "切换字段详细信息", - "kbnESQuery.kql.errors.endOfInputText": "输入结束", - "kbnESQuery.kql.errors.fieldNameText": "字段名称", - "kbnESQuery.kql.errors.literalText": "文本", - "kbnESQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", - "kbnESQuery.kql.errors.valueText": "值", - "kbnESQuery.kql.errors.whitespaceText": "空白", + "data.common.esQuery.kql.errors.endOfInputText": "输入结束", + "data.common.esQuery.kql.errors.fieldNameText": "字段名称", + "data.common.esQuery.kql.errors.literalText": "文本", + "data.common.esQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", + "data.common.esQuery.kql.errors.valueText": "值", + "data.common.esQuery.kql.errors.whitespaceText": "空白", "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", "kbnVislibVisTypes.area.areaTitle": "面积图", "kbnVislibVisTypes.area.groupTitle": "拆分序列", @@ -3360,7 +3357,6 @@ "xpack.apm.errorsTable.occurrencesColumnLabel": "发生次数", "xpack.apm.errorsTable.unhandledLabel": "未处理", "xpack.apm.featureRegistry.apmFeatureName": "APM", - "xpack.apm.feedbackMenu.provideFeedbackTitle": "提供 APM 的反馈", "xpack.apm.filter.environment.allLabel": "全部", "xpack.apm.filter.environment.label": "环境", "xpack.apm.filter.environment.notDefinedLabel": "未定义", @@ -5847,7 +5843,6 @@ "xpack.infra.homePage.noMetricsIndicesTitle": "似乎您没有任何指标索引。", "xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "搜索基础设施数据……(例如 host.name:host-1)", "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "在选定时间显示过去 1 分钟的数据", - "xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText": "提供 Infrastructure 的反馈", "xpack.infra.infrastructureDescription": "浏览您的基础设施", "xpack.infra.infrastructureMetricsExplorerPage.documentTitle": "{previousTitle} | 指标浏览器", "xpack.infra.infrastructureSnapshotPage.documentTitle": "{previousTitle} | 库存", @@ -5898,7 +5893,6 @@ "xpack.infra.logs.stopStreamingButtonLabel": "停止流式传输", "xpack.infra.logs.streamingDescription": "正在流式传输新条目……", "xpack.infra.logs.streamingNewEntriesText": "正在流式传输新条目", - "xpack.infra.logsPage.logsHelpContent.feedbackLinkText": "提供 Logs 的反馈", "xpack.infra.logsPage.noLoggingIndicesDescription": "让我们添加一些!", "xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "查看设置说明", "xpack.infra.logsPage.noLoggingIndicesTitle": "似乎您没有任何日志索引。", @@ -6422,8 +6416,6 @@ "xpack.maps.heatmap.colorRampLabel": "颜色范围", "xpack.maps.heatmapLegend.coldLabel": "冷", "xpack.maps.heatmapLegend.hotLabel": "热", - "xpack.maps.helpMenu.docLabel": "地图文档", - "xpack.maps.helpMenu.feedbackLinkText": "提供 Maps 应用程序的反馈", "xpack.maps.inspector.centerLatLabel": "中心纬度", "xpack.maps.inspector.centerLonLabel": "中心经度", "xpack.maps.inspector.mapboxStyleTitle": "Mapbox 样式", @@ -6623,7 +6615,6 @@ "xpack.maps.source.wmsTitle": "Web 地图服务", "xpack.maps.style.heatmap.displayNameLabel": "热图样式", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}", - "xpack.maps.style.vector.displayNameLabel": "矢量样式", "xpack.maps.styles.staticDynamic.dynamicDescription": "使用属性值代表功能。", "xpack.maps.styles.staticDynamic.staticDescription": "使用静态样式属性代表功能。", "xpack.maps.styles.vector.borderColorLabel": "边框颜色", @@ -7756,7 +7747,6 @@ "xpack.ml.dataframe.analytics.exploration.indexObjectToolTipContent": "无法显示此基于对象的列的完整内容。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "作业 ID {jobId}", "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutTitle": "空的索引查询结果。", "xpack.ml.dataframe.analytics.exploration.selectColumnsAriaLabel": "选择列", "xpack.ml.dataframe.analytics.exploration.selectFieldsPopoverTitle": "选择字段", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", @@ -10028,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", @@ -10457,8 +10446,6 @@ "xpack.siem.certificate.fingerprint.clientCertLabel": "客户端证书", "xpack.siem.certificate.fingerprint.serverCertLabel": "服务器证书", "xpack.siem.chart.dataNotAvailableTitle": "图表数据不可用", - "xpack.siem.chrome.help.feedback": "提交反馈", - "xpack.siem.chrome.help.title": "SIEM 应用程序帮助", "xpack.siem.clipboard.copied": "已复制", "xpack.siem.clipboard.copy": "复制", "xpack.siem.clipboard.to.the.clipboard": "至剪贴板", @@ -11889,8 +11876,6 @@ "xpack.uptime.filterBar.options.location.name": "位置", "xpack.uptime.filterBar.options.portLabel": "端口", "xpack.uptime.filterBar.options.schemeLabel": "方案", - "xpack.uptime.header.helpLinkAriaLabel": "前往我们的讨论页", - "xpack.uptime.header.helpLinkText": "提供运行时间反馈", "xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。", "xpack.uptime.monitorCharts.checkStatus.series.downCountLabel": "关闭计数", "xpack.uptime.monitorCharts.checkStatus.series.upCountLabel": "运行计数", @@ -11952,8 +11937,6 @@ "xpack.uptime.snapshotHistogram.series.upLabel": "运行", "xpack.uptime.uptimeFeatureCatalogueTitle": "运行时间", "xpack.uptime.emptyState.noDataMessage": "未找到任何运行时间数据", - "xpack.uptime.header.docsLinkAriaLabel": "前往 Uptime 文档", - "xpack.uptime.header.documentationLinkText": "Uptime 文档", "xpack.uptime.pingList.collapseRow": "折叠", "xpack.uptime.pingList.durationMsColumnFormatting": "{millis} 毫秒", "xpack.uptime.pingList.expandedRow.bodySize": "正文大小为 {bodyBytes}。", @@ -12756,8 +12739,6 @@ "xpack.lens.functions.mergeTables.help": "将任何数目的 kibana 表合并成单个表的助手", "xpack.lens.functions.renameColumns.help": "用于重命名数据表列的助手", "xpack.lens.functions.renameColumns.idMap.help": "旧列 ID 为键且相应新列 ID 为值的 JSON 编码对象。所有其他列 ID 都将保留。", - "xpack.lens.helpMenu.docLabel": "Lens 文档", - "xpack.lens.helpMenu.feedbackLinkText": "提供 Lens 应用程序的反馈", "xpack.lens.indexPattern.avg": "平均值", "xpack.lens.indexPattern.avgOf": "{name} 的平均值", "xpack.lens.indexPattern.cardinality": "唯一计数", diff --git a/x-pack/tasks/test.ts b/x-pack/tasks/test.ts index d26683899ce3f..0767d7479724a 100644 --- a/x-pack/tasks/test.ts +++ b/x-pack/tasks/test.ts @@ -5,35 +5,12 @@ */ import pluginHelpers from '@kbn/plugin-helpers'; -import { createAutoJUnitReporter } from '@kbn/test'; -// @ts-ignore no types available -import mocha from 'gulp-mocha'; import gulp from 'gulp'; import { getEnabledPlugins } from './helpers/flags'; export const testServerTask = async () => { - const pluginIds = await getEnabledPlugins(); - - const testGlobs = ['common/**/__tests__/**/*.js', 'server/**/__tests__/**/*.js']; - - for (const pluginId of pluginIds) { - testGlobs.push( - `legacy/plugins/${pluginId}/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/common/**/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/**/server/**/__tests__/**/*.js` - ); - } - - return gulp.src(testGlobs, { read: false }).pipe( - mocha({ - ui: 'bdd', - require: require.resolve('../../src/setup_node_env'), - reporter: createAutoJUnitReporter({ - reportName: 'X-Pack Mocha Tests', - }), - }) - ); + throw new Error('server mocha tests are now included in the `node scripts/mocha` script'); }; export const testBrowserTask = async () => { @@ -51,4 +28,4 @@ export const testBrowserDevTask = async () => { }); }; -export const testTask = gulp.series(testServerTask, testBrowserTask); +export const testTask = gulp.series(testBrowserTask, testServerTask); diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 4fbb13b229003..57b4b3b6c26c6 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -183,7 +183,7 @@ export class AlertUtils { throttle: '1m', tags: [], alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts index d7fba7e43c372..ae382652b6234 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts @@ -13,7 +13,7 @@ export function getTestAlertData(overwrites = {}) { interval: '1m', throttle: '1m', actions: [], - alertTypeParams: {}, + params: {}, ...overwrites, }; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index b9b40be3a04b3..648944a9256b2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -12,7 +12,7 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; // eslint-disable-next-line import/no-default-export export default function indexTest({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 44970a9260c42..0c05ad3e3e68a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -18,7 +18,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 0a300c4ce65da..09a642d1d14bb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -20,7 +20,7 @@ import { // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -166,7 +166,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -258,7 +258,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -356,7 +356,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, @@ -491,7 +491,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', 'other'], @@ -560,7 +560,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', null, 'default'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index bd0226d024d1f..bf61ee2e3f137 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('create', () => { @@ -59,7 +59,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: user.username, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, @@ -173,10 +173,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['name', 'alertTypeId', 'interval', 'alertTypeParams', 'actions'], + keys: ['name', 'alertTypeId', 'interval', 'params', 'actions'], }, }); break; @@ -185,7 +185,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it(`should handle create alert request appropriately when alertTypeParams isn't valid`, async () => { + it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -214,7 +214,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 6d5147e9f87b8..aab683df09740 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('delete', () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index 8a9b7e3fc35c4..d2076e0f92b3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 543805fb83b18..528db61dba21c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index b04c0f44e7dd4..31af7a0acffbb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -62,7 +62,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', @@ -119,7 +119,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index cfb2f34ca8056..1a8109f6b6b3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -56,7 +56,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: response.body.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 78f70ddb13edd..1b1bcef9ad23f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -33,7 +33,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -93,7 +93,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -142,7 +142,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], throttle: '1m', alertTypeId: '1', - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -203,10 +203,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['throttle', 'name', 'tags', 'interval', 'alertTypeParams', 'actions'], + keys: ['throttle', 'name', 'tags', 'interval', 'params', 'actions'], }, }); break; @@ -222,7 +222,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.validation', - alertTypeParams: { + params: { param1: 'test', }, }) @@ -239,7 +239,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], interval: '1m', throttle: '1m', - alertTypeParams: {}, + params: {}, actions: [], }); @@ -261,7 +261,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 74a1255dacfe5..7e971d033b5c4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -12,7 +12,7 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; // eslint-disable-next-line import/no-default-export export default function indexTest({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index ba8a5aa9160a5..e97b6d480c470 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -17,7 +17,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index badec079d6828..9af4848c57d7d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -125,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { getTestAlertData({ interval: '1m', alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -193,7 +193,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -238,7 +238,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 17ec83b5b4f18..3018f8efffffe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); describe('create', () => { const objectRemover = new ObjectRemover(supertest); @@ -41,7 +41,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: null, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 3ef501cfaa588..3aea982f948ea 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); describe('delete', () => { const objectRemover = new ObjectRemover(supertest); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 664e74835d415..750f94201216a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('disable', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 2a8de1f6e31c3..00cd40c0e80cd 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('enable', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f49d774fc1e92..0d12af6db79b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -45,7 +45,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index ef27a2713e98a..9e4797bcbf7ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -39,7 +39,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index 942eff0766722..a6eccf88d9e26 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -28,7 +28,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -68,7 +68,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['foo'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js index 27435d49c252f..e4637d3807d4d 100644 --- a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js +++ b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js @@ -10,7 +10,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const randomness = getService('randomness'); describe('assign_tags_to_beats', () => { diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js index 6cd4fdf22bb41..09cfb33e4fad2 100644 --- a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js +++ b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js @@ -10,7 +10,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); describe('create_enrollment_token', () => { it('should create one token by default', async () => { diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 4b4767e1d9849..59c42db7c1f81 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -12,7 +12,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const randomness = getService('randomness'); - const es = getService('es'); + const es = getService('legacyEs'); describe('enroll_beat', () => { let validEnrollmentToken; diff --git a/x-pack/test/api_integration/apis/beats/get_beat.js b/x-pack/test/api_integration/apis/beats/get_beat.js index 07cc056e3af99..03667a53920c9 100644 --- a/x-pack/test/api_integration/apis/beats/get_beat.js +++ b/x-pack/test/api_integration/apis/beats/get_beat.js @@ -10,7 +10,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); describe('get_beat_configuration', () => { const archive = 'beats/list'; diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index da47fdbf77fc7..8ca7390ad5b1f 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -7,7 +7,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService, loadTestFile }) { - const es = getService('es'); + const es = getService('legacyEs'); describe('beats', () => { const cleanup = () => diff --git a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js index 1548aff1182b3..dde8916dd24d5 100644 --- a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js +++ b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js @@ -10,7 +10,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const randomness = getService('randomness'); describe('remove_tags_from_beats', () => { diff --git a/x-pack/test/api_integration/apis/beats/set_config.js b/x-pack/test/api_integration/apis/beats/set_config.js index 5e15145cf47c8..21a09333dc31a 100644 --- a/x-pack/test/api_integration/apis/beats/set_config.js +++ b/x-pack/test/api_integration/apis/beats/set_config.js @@ -9,7 +9,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('set_config', () => { diff --git a/x-pack/test/api_integration/apis/beats/set_tag.js b/x-pack/test/api_integration/apis/beats/set_tag.js index ee9a601b25096..630c3772b1661 100644 --- a/x-pack/test/api_integration/apis/beats/set_tag.js +++ b/x-pack/test/api_integration/apis/beats/set_tag.js @@ -9,7 +9,7 @@ import { ES_INDEX_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); describe('set_tag', () => { it('should create a tag', async () => { diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index 228d651a590e6..82582b553886c 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -11,7 +11,7 @@ import moment from 'moment'; export default function ({ getService }) { const supertest = getService('supertest'); const randomness = getService('randomness'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); describe('update_beat', () => { diff --git a/x-pack/test/api_integration/apis/es/has_privileges.js b/x-pack/test/api_integration/apis/es/has_privileges.js index 5cbbefd8f2f37..37d6d73499552 100644 --- a/x-pack/test/api_integration/apis/es/has_privileges.js +++ b/x-pack/test/api_integration/apis/es/has_privileges.js @@ -11,7 +11,7 @@ export default function ({ getService }) { describe('has_privileges', () => { before(async () => { - const es = getService('es'); + const es = getService('legacyEs'); await es.shield.postPrivileges({ body: { @@ -104,7 +104,7 @@ export default function ({ getService }) { }); // Create privilege - const es = getService('es'); + const es = getService('legacyEs'); await es.shield.postPrivileges({ body: { [application]: { diff --git a/x-pack/test/api_integration/apis/es/post_privileges.js b/x-pack/test/api_integration/apis/es/post_privileges.js index 4b1695487f832..1c8f723b7a278 100644 --- a/x-pack/test/api_integration/apis/es/post_privileges.js +++ b/x-pack/test/api_integration/apis/es/post_privileges.js @@ -9,7 +9,7 @@ export default function ({ getService }) { describe('post_privileges', () => { it('should allow privileges to be updated', async () => { - const es = getService('es'); + const es = getService('legacyEs'); const application = 'foo'; const response = await es.shield.postPrivileges({ body: { diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 86ef445899039..ca339e9f407f2 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -27,5 +27,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./siem')); loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); + loadTestFile(require.resolve('./licensing')); }); } diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 6428ef9f478d4..5e6830c8f4689 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -22,7 +22,7 @@ const COMMON_HEADERS = { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es: Client = getService('es'); + const es: Client = getService('legacyEs'); const callCluster: CallCluster = (((path: 'search', searchParams: SearchParams) => { return es[path].call(es, searchParams); }) as unknown) as CallCluster; diff --git a/x-pack/test/api_integration/apis/licensing/index.ts b/x-pack/test/api_integration/apis/licensing/index.ts new file mode 100644 index 0000000000000..f14d5102f6f4e --- /dev/null +++ b/x-pack/test/api_integration/apis/licensing/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function licensingIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Licensing', () => { + loadTestFile(require.resolve('./info')); + }); +} diff --git a/x-pack/test/api_integration/apis/licensing/info.ts b/x-pack/test/api_integration/apis/licensing/info.ts new file mode 100644 index 0000000000000..0b48080616fb9 --- /dev/null +++ b/x-pack/test/api_integration/apis/licensing/info.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Info', () => { + describe('GET /api/licensing/info', () => { + it('returns licensing information', async () => { + const response = await supertest.get('/api/licensing/info').expect(200); + + expect(response.body).property('features'); + expect(response.body).property('license'); + expect(response.body).property('signature'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/cluster/load.js b/x-pack/test/api_integration/apis/logstash/cluster/load.js index a20c524f0a8f8..36b992559e15b 100644 --- a/x-pack/test/api_integration/apis/logstash/cluster/load.js +++ b/x-pack/test/api_integration/apis/logstash/cluster/load.js @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); describe('load', () => { it('should return the ES cluster info', async () => { diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index 8ed10ccf31dce..cd128d92498cf 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -14,7 +14,7 @@ import { registerHelpers as registerFollowerIndicesnHelpers } from './follower_i export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { addCluster, deleteAllClusters } = registerRemoteClustersHelpers(supertest); const { diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js index d0d08101e5444..020d6fc5741b9 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js @@ -13,7 +13,7 @@ import { getPolicyPayload } from './fixtures'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { getIndex, diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js index 5ce1d7d956d67..bc8b2af401423 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js @@ -13,7 +13,7 @@ import { initElasticsearchHelpers } from './lib'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { getNodesStats } = initElasticsearchHelpers(es); const { loadNodes, getNodeDetails } = registerHelpers({ supertest }); diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index c13ae4a15c97e..598db2ddc8a65 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -15,7 +15,7 @@ import { DEFAULT_POLICY_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndex, diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js index 7dcf3c995e3ce..5c6e3d0e89c81 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js @@ -12,7 +12,7 @@ import { registerHelpers as registerPoliciesHelpers } from './policies.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndexTemplate, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 09215eff20c69..9302b0fe0e16b 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -11,7 +11,7 @@ import { registerHelpers } from './indices.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndex, diff --git a/x-pack/test/api_integration/apis/management/index_management/mapping.js b/x-pack/test/api_integration/apis/management/index_management/mapping.js index 91f995a7d8045..0a2713e9407f1 100644 --- a/x-pack/test/api_integration/apis/management/index_management/mapping.js +++ b/x-pack/test/api_integration/apis/management/index_management/mapping.js @@ -11,7 +11,7 @@ import { registerHelpers } from './mapping.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndex, diff --git a/x-pack/test/api_integration/apis/management/index_management/settings.js b/x-pack/test/api_integration/apis/management/index_management/settings.js index dc41f530085b1..d9a9e19fe2490 100644 --- a/x-pack/test/api_integration/apis/management/index_management/settings.js +++ b/x-pack/test/api_integration/apis/management/index_management/settings.js @@ -11,7 +11,7 @@ import { registerHelpers } from './settings.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndex, diff --git a/x-pack/test/api_integration/apis/management/index_management/stats.js b/x-pack/test/api_integration/apis/management/index_management/stats.js index 58dac9f5b911a..743b1f596e9d7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/stats.js +++ b/x-pack/test/api_integration/apis/management/index_management/stats.js @@ -11,7 +11,7 @@ import { registerHelpers } from './stats.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndex, diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index a6a7493218499..00a97e55c013c 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -11,7 +11,7 @@ import { registerHelpers } from './templates.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { cleanUp: cleanUpEsResources, diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index 25bc503682bc7..78a04b729ba66 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -13,7 +13,7 @@ import { getRandomString } from './lib'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndexWithMappings, diff --git a/x-pack/test/api_integration/apis/management/rollup/rollup.js b/x-pack/test/api_integration/apis/management/rollup/rollup.js index 5ac4e06adb23a..6e99af2ba8e32 100644 --- a/x-pack/test/api_integration/apis/management/rollup/rollup.js +++ b/x-pack/test/api_integration/apis/management/rollup/rollup.js @@ -11,7 +11,7 @@ import { registerHelpers } from './rollup.test_helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndexWithMappings, diff --git a/x-pack/test/api_integration/apis/management/rollup/rollup_search.js b/x-pack/test/api_integration/apis/management/rollup/rollup_search.js index 2a38cd563f312..073473f202fa3 100644 --- a/x-pack/test/api_integration/apis/management/rollup/rollup_search.js +++ b/x-pack/test/api_integration/apis/management/rollup/rollup_search.js @@ -12,7 +12,7 @@ import { getRandomString } from './lib'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('es'); + const es = getService('legacyEs'); const { createIndexWithMappings, diff --git a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js index 6d3dbe68c77ac..2443448ccd062 100644 --- a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js +++ b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js @@ -13,7 +13,7 @@ import * as beatsMetrics from '../../../../../legacy/plugins/monitoring/server/l import * as apmMetrics from '../../../../../legacy/plugins/monitoring/server/lib/metrics/apm/metrics'; export default function ({ getService }) { - const es = getService('es'); + const es = getService('legacyEs'); const metricSets = [ { diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fc..052d984774e69 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js index d1d4c3c7b7af8..7108656783f52 100644 --- a/x-pack/test/api_integration/apis/security/roles.js +++ b/x-pack/test/api_integration/apis/security/roles.js @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); const config = getService('config'); const basic = config.get('esTestCluster.license') === 'basic'; diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 0000000000000..7c7883f58cb30 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts index de03fff7edcf7..582864795015c 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -10,7 +10,7 @@ import { TelemetrySavedObjectAttributes } from '../../../../../src/legacy/core_p import { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { - const client: Client = getService('es'); + const client: Client = getService('legacyEs'); const supertest = getService('supertest'); describe('/api/telemetry/v2/optIn API Telemetry User has seen OptIn Notice', () => { diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json index 0ac6c67e23d2b..93d63bad66e30 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 7, - "mixed": 0, - "up": 93, - "total": 100 - } - } + "up": 93, + "down": 7, + "mixed": 0, + "total": 100 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json index 065c3f90e932e..2d79880e7c0ee 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 0, - "mixed": 0, - "up": 94, - "total": 94 - } - } + "up": 93, + "down": 0, + "mixed": 0, + "total": 93 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.js b/x-pack/test/api_integration/apis/uptime/graphql/index.js index f7fafa9419657..346032f87dc4d 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.js +++ b/x-pack/test/api_integration/apis/uptime/graphql/index.js @@ -17,7 +17,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./monitor_states')); loadTestFile(require.resolve('./monitor_status_bar')); loadTestFile(require.resolve('./ping_list')); - loadTestFile(require.resolve('./snapshot')); loadTestFile(require.resolve('./snapshot_histogram')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts b/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts index 9dca3f40c6303..c305bb99c28f7 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts @@ -106,7 +106,7 @@ export default function({ getService }: FtrProviderContext) { before(async () => { const index = 'heartbeat-8.0.0'; - const es = getService('es'); + const es = getService('legacyEs'); dateRangeStart = new Date().toISOString(); checks = await makeChecks(es, index, testMonitorId, 1, numIps, {}, d => { if (d.summary) { diff --git a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js b/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js deleted file mode 100644 index 004b87571eab4..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js +++ /dev/null @@ -1,101 +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 { snapshotQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function ({ getService }) { - describe('snapshot query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('will fetch a monitor snapshot summary', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot'); - }); - - it('will fetch a monitor snapshot filtered by down status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - statusFilter: 'down', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot_filtered_by_down'); - }); - - it('will fetch a monitor snapshot filtered by up status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`, - statusFilter: 'up', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_filtered_by_up'); - }); - - it('returns null histogram data when no data present', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-25T04:30:54.740Z', - dateRangeEnd: '2025-01-28T04:50:54.740Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_empty'); - }); - // TODO: test for host, port, etc. - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/index.js b/x-pack/test/api_integration/apis/uptime/index.js index 8f4e4ab9a7ea1..9175658783fb5 100644 --- a/x-pack/test/api_integration/apis/uptime/index.js +++ b/x-pack/test/api_integration/apis/uptime/index.js @@ -5,7 +5,7 @@ */ export default function ({ getService, loadTestFile }) { - const es = getService('es'); + const es = getService('legacyEs'); describe('uptime', () => { before(() => @@ -17,5 +17,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); loadTestFile(require.resolve('./graphql')); + loadTestFile(require.resolve('./rest')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts new file mode 100644 index 0000000000000..b76d3f7c2e44a --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + describe('uptime REST endpoints', () => { + before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); + after('unload', () => esArchiver.unload('uptime/full_heartbeat')); + loadTestFile(require.resolve('./snapshot')); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts new file mode 100644 index 0000000000000..0175dc649b495 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('snapshot count', () => { + let dateRangeStart = '2019-01-28T17:40:08.078Z'; + let dateRangeEnd = '2025-01-28T19:00:16.078Z'; + + it('will fetch the full set of snapshot counts', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + ); + expectFixtureEql(apiResponse.body, 'snapshot'); + }); + + it('will fetch a monitor snapshot filtered by down status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const statusFilter = 'down'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); + }); + + it('will fetch a monitor snapshot filtered by up status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`; + const statusFilter = 'up'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + }); + + it('returns a null snapshot when no data is present', async () => { + dateRangeStart = '2019-01-25T04:30:54.740Z'; + dateRangeEnd = '2025-01-28T04:50:54.740Z'; + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_empty'); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a..9c67dfe61b957 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 7e3b747c81993..4be89172e24f0 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -30,7 +30,7 @@ export const services = { esSupertest: kibanaApiIntegrationServices.esSupertest, supertest: kibanaApiIntegrationServices.supertest, - es: LegacyEsProvider, + legacyEs: LegacyEsProvider, esSupertestWithoutAuth: EsSupertestWithoutAuthProvider, infraOpsGraphQLClient: InfraOpsGraphQLClientProvider, infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider, diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 21fc1982edc42..efcfaaba6037c 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 67bc8bd38ff1c..7cbab3cdcf4f2 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -25,9 +25,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); async function setDiscoverTimeRange() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); } describe('security', () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 243d5a0b6b6d0..4c2a8b4bed306 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -21,9 +21,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); async function setDiscoverTimeRange() { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); } describe('spaces', () => { diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index cb6f0b6028a2d..f640a34b36ddf 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,7 +13,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('graph', function() { + // FLAKY: https://github.com/elastic/kibana/issues/45321 + describe.skip('graph', function() { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index ba307a24cd739..d5d617587fc3b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -7,6 +7,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('anomaly detection', function() { + this.tags(['skipFirefox']); + loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 1634bea47a69f..30b957fdf45f4 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, true, true]); @@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, false, false]); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 49519b530337e..bfa4be2b067af 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -15,7 +15,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -89,7 +89,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -160,7 +160,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ diff --git a/x-pack/test/functional/apps/monitoring/beats/beat_detail.js b/x-pack/test/functional/apps/monitoring/beats/beat_detail.js index d352579e01160..d17e233a484e8 100644 --- a/x-pack/test/functional/apps/monitoring/beats/beat_detail.js +++ b/x-pack/test/functional/apps/monitoring/beats/beat_detail.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/beats', { - from: '2017-12-19 17:14:09.000', - to: '2017-12-19 18:15:09.000', + from: 'Dec 19, 2017 @ 17:14:09.000', + to: 'Dec 19, 2017 @ 18:15:09.000', }); // go to beats detail diff --git a/x-pack/test/functional/apps/monitoring/beats/cluster.js b/x-pack/test/functional/apps/monitoring/beats/cluster.js index 8ee93066254d5..9a2532adfbae8 100644 --- a/x-pack/test/functional/apps/monitoring/beats/cluster.js +++ b/x-pack/test/functional/apps/monitoring/beats/cluster.js @@ -15,8 +15,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/beats', { - from: '2017-12-19 17:14:09.000', - to: '2017-12-19 18:15:09.000', + from: 'Dec 19, 2017 @ 17:14:09.000', + to: 'Dec 19, 2017 @ 18:15:09.000', }); }); diff --git a/x-pack/test/functional/apps/monitoring/beats/listing.js b/x-pack/test/functional/apps/monitoring/beats/listing.js index 700b5d593ecb8..7a27a7b5f219d 100644 --- a/x-pack/test/functional/apps/monitoring/beats/listing.js +++ b/x-pack/test/functional/apps/monitoring/beats/listing.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/beats', { - from: '2017-12-19 17:14:09.000', - to: '2017-12-19 18:15:09.000', + from: 'Dec 19, 2017 @ 17:14:09.000', + to: 'Dec 19, 2017 @ 18:15:09.000', }); // go to beats listing diff --git a/x-pack/test/functional/apps/monitoring/beats/overview.js b/x-pack/test/functional/apps/monitoring/beats/overview.js index 1c8b70a462843..16c198be0432f 100644 --- a/x-pack/test/functional/apps/monitoring/beats/overview.js +++ b/x-pack/test/functional/apps/monitoring/beats/overview.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/beats', { - from: '2017-12-19 17:14:09.000', - to: '2017-12-19 18:15:09.000', + from: 'Dec 19, 2017 @ 17:14:09.000', + to: 'Dec 19, 2017 @ 18:15:09.000', }); // go to beats overview diff --git a/x-pack/test/functional/apps/monitoring/cluster/alerts.js b/x-pack/test/functional/apps/monitoring/cluster/alerts.js index 1e61518947d2e..2a0dbec6f3b9e 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/alerts.js +++ b/x-pack/test/functional/apps/monitoring/cluster/alerts.js @@ -23,8 +23,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:23:47.528', - to: '2017-08-29 17:25:50.701', + from: 'Aug 29, 2017 @ 17:23:47.528', + to: 'Aug 29, 2017 @ 17:25:50.701', }); // ensure cluster alerts are shown on overview @@ -51,8 +51,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum--with-10-alerts', { - from: '2017-08-29 17:23:47.528', - to: '2017-08-29 17:25:50.701', + from: 'Aug 29, 2017 @ 17:23:47.528', + to: 'Aug 29, 2017 @ 17:25:50.701', }); // ensure cluster alerts are shown on overview @@ -166,8 +166,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:23:47.528', - to: '2017-08-29 17:25:50.701', + from: 'Aug 29, 2017 @ 17:23:47.528', + to: 'Aug 29, 2017 @ 17:25:50.701', }); // ensure cluster alerts are shown on overview diff --git a/x-pack/test/functional/apps/monitoring/cluster/list.js b/x-pack/test/functional/apps/monitoring/cluster/list.js index 70e89689a89db..399b392cad797 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/list.js +++ b/x-pack/test/functional/apps/monitoring/cluster/list.js @@ -21,8 +21,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/multicluster', { - from: '2017-08-15 21:00:00.000', - to: '2017-08-16 00:00:00.000', + from: 'Aug 15, 2017 @ 21:00:00.000', + to: 'Aug 16, 2017 @ 00:00:00.000', }); await clusterList.assertDefaults(); @@ -76,8 +76,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/multi-basic', { - from: '2017-09-07 20:12:04.011', - to: '2017-09-07 20:18:55.733', + from: 'Sep 7, 2017 @ 20:12:04.011', + to: 'Sep 7, 2017 @ 20:18:55.733', }); await clusterList.assertDefaults(); diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 8242150e404eb..3396426e95380 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -16,8 +16,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-green-gold', { - from: '2017-08-23 21:29:35.267', - to: '2017-08-23 21:47:25.556', + from: 'Aug 23, 2017 @ 21:29:35.267', + to: 'Aug 23, 2017 @ 21:47:25.556', }); }); @@ -71,8 +71,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:23:47.528', - to: '2017-08-29 17:25:50.701', + from: 'Aug 29, 2017 @ 17:23:47.528', + to: 'Aug 29, 2017 @ 17:25:50.701', }); }); @@ -121,8 +121,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-basic', { - from: '2017-08-29 17:55:43.879', - to: '2017-08-29 18:01:34.958', + from: 'Aug 29, 2017 @ 17:55:43.879', + to: 'Aug 29, 2017 @ 18:01:34.958', }); }); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js index c3fe5f9273a89..7109f9363e1b7 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js @@ -25,8 +25,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 20:31:48.354', - to: '2017-10-05 20:35:12.176' + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176' }); // go to indices listing @@ -83,8 +83,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-red-platinum', { - from: '2017-10-06 19:53:06.748', - to: '2017-10-06 20:15:30.212' + from: 'Oct 6, 2017 @ 19:53:06.748', + to: 'Oct 6, 2017 @ 20:15:30.212' }); // go to indices listing diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js index 1ed7c15a9ecf1..6500f373807a4 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-red-platinum', { - from: '2017-10-06 19:53:06.748', - to: '2017-10-06 20:15:30.212', + from: 'Oct 6, 2017 @ 19:53:06.748', + to: 'Oct 6, 2017 @ 20:15:30.212', }); // go to indices listing diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js index 99dce6c1b89b6..6521753de506a 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js @@ -19,8 +19,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 20:31:48.354', - to: '2017-10-05 20:35:12.176' + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176' }); // go to nodes listing @@ -74,8 +74,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-red-platinum', { - from: '2017-10-06 19:53:06.748', - to: '2017-10-06 20:15:30.212' + from: 'Oct 6, 2017 @ 19:53:06.748', + to: 'Oct 6, 2017 @ 20:15:30.212' }); // go to nodes listing diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js index 7ad09e034e13b..94ad5e493d73e 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js @@ -21,8 +21,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 20:28:28.475', - to: '2017-10-05 20:34:38.341', + from: 'Oct 5, 2017 @ 20:28:28.475', + to: 'Oct 5, 2017 @ 20:34:38.341', }); // go to nodes listing @@ -196,8 +196,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 20:31:48.354', - to: '2017-10-05 20:35:12.176', + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176', }); // go to nodes listing diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js index a0e91e2336e08..b5429cdc891f9 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 20:31:48.354', - to: '2017-10-05 20:35:12.176' + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176' }); // go to overview diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/shards.js b/x-pack/test/functional/apps/monitoring/elasticsearch/shards.js index ab02eead16333..4a40d686af496 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/shards.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/shards.js @@ -21,8 +21,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-three-nodes-shard-relocation', { - from: '2017-10-05 19:34:48.000', - to: '2017-10-05 20:35:12.000', + from: 'Oct 5, 2017 @ 19:34:48.000', + to: 'Oct 5, 2017 @ 20:35:12.000', }); }); diff --git a/x-pack/test/functional/apps/monitoring/kibana/instance.js b/x-pack/test/functional/apps/monitoring/kibana/instance.js index 33448fc4b206a..0d4e06fb2b98f 100644 --- a/x-pack/test/functional/apps/monitoring/kibana/instance.js +++ b/x-pack/test/functional/apps/monitoring/kibana/instance.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:24:14.254', - to: '2017-08-29 17:25:44.142', + from: 'Aug 29, 2017 @ 17:24:14.254', + to: 'Aug 29, 2017 @ 17:25:44.142', }); // go to kibana instance diff --git a/x-pack/test/functional/apps/monitoring/kibana/instances.js b/x-pack/test/functional/apps/monitoring/kibana/instances.js index 28bf108d59f24..8f72806eee441 100644 --- a/x-pack/test/functional/apps/monitoring/kibana/instances.js +++ b/x-pack/test/functional/apps/monitoring/kibana/instances.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:24:14.254', - to: '2017-08-29 17:25:44.142', + from: 'Aug 29, 2017 @ 17:24:14.254', + to: 'Aug 29, 2017 @ 17:25:44.142', }); // go to kibana instances diff --git a/x-pack/test/functional/apps/monitoring/kibana/overview.js b/x-pack/test/functional/apps/monitoring/kibana/overview.js index 7282e48fc8a91..c5da75e37a77f 100644 --- a/x-pack/test/functional/apps/monitoring/kibana/overview.js +++ b/x-pack/test/functional/apps/monitoring/kibana/overview.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/singlecluster-yellow-platinum', { - from: '2017-08-29 17:24:14.254', - to: '2017-08-29 17:25:44.142', + from: 'Aug 29, 2017 @ 17:24:14.254', + to: 'Aug 29, 2017 @ 17:25:44.142', }); // go to kibana overview diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js index fb9fcf8bab8bb..f4d2a5a4a20a5 100644 --- a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js +++ b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js @@ -19,8 +19,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/logstash-pipelines', { - from: '2018-01-22 09:10:00.000', - to: '2018-01-22 09:41:00.000', + from: 'Jan 22, 2018 @ 09:10:00.000', + to: 'Jan 22, 2018 @ 09:41:00.000', }); // go to pipelines listing diff --git a/x-pack/test/functional/apps/monitoring/time_filter.js b/x-pack/test/functional/apps/monitoring/time_filter.js index 0afcada14be5f..ae5b11e8e0b32 100644 --- a/x-pack/test/functional/apps/monitoring/time_filter.js +++ b/x-pack/test/functional/apps/monitoring/time_filter.js @@ -17,8 +17,8 @@ export default function ({ getService, getPageObjects }) { before(async () => { await setup('monitoring/multicluster', { - from: '2017-08-15 21:00:00.000', - to: '2017-08-16 00:00:00.000', + from: 'Aug 15, 2017 @ 21:00:00.000', + to: 'Aug 16, 2017 @ 00:00:00.000', }); await clusterList.assertDefaults(); }); @@ -35,7 +35,7 @@ export default function ({ getService, getPageObjects }) { }); it('should send another request when changing the time picker', async () => { - await PageObjects.timePicker.setAbsoluteRange('2016-08-15 21:00:00.000', '2016-08-16 00:00:00.000'); + await PageObjects.timePicker.setAbsoluteRange('Aug 15, 2016 @ 21:00:00.000', 'Aug 16, 2016 @ 00:00:00.000'); await clusterList.assertNoData(); }); }); diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index dc47bd9de3815..9a4bc5b6a5cbd 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index dca5534fb68b3..95bae4b24a535 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -11,7 +11,7 @@ import mockRolledUpData, { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'settings']); diff --git a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js index eeff20105aed2..0888f0972592e 100644 --- a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js +++ b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js @@ -11,7 +11,7 @@ import { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['rollup', 'common']); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 9b8be614c9f5b..3f9f2f6bdbe87 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -85,8 +85,6 @@ export default function ({ getService, getPageObjects }) { // this is to acertain that all role assigned to the user can perform actions like creating a Visualization it('rbac all role can save a visualization', async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; const vizName1 = 'Visualization VerticalBarChart'; log.debug('log in as kibanauser with rbac_all role'); @@ -96,8 +94,10 @@ export default function ({ getService, getPageObjects }) { log.debug('clickVerticalBarChart'); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); - log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + log.debug('Set absolute time range from \"' + + PageObjects.timePicker.defaultStartTime + '\" to \"' + + PageObjects.timePicker.defaultEndTime + '\"'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.waitForVisualization(); await PageObjects.visualize.saveVisualizationExpectSuccess(vizName1); await PageObjects.security.logout(); diff --git a/x-pack/test/functional/apps/transform/creation.ts b/x-pack/test/functional/apps/transform/creation.ts deleted file mode 100644 index 3ab17c0d90a83..0000000000000 --- a/x-pack/test/functional/apps/transform/creation.ts +++ /dev/null @@ -1,229 +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 { FtrProviderContext } from '../../ftr_provider_context'; - -interface GroupByEntry { - identifier: string; - label: string; - intervalLabel?: string; -} - -export default function({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const transform = getService('transform'); - - describe('creation', function() { - this.tags(['smoke']); - before(async () => { - await esArchiver.load('ml/ecommerce'); - }); - - after(async () => { - await esArchiver.unload('ml/ecommerce'); - await transform.api.cleanTransformIndices(); - }); - - const testDataList = [ - { - suiteTitle: 'batch transform with terms+date_histogram groups and avg agg', - source: 'ecommerce', - groupByEntries: [ - { - identifier: 'terms(category.keyword)', - label: 'category.keyword', - } as GroupByEntry, - { - identifier: 'date_histogram(order_date)', - label: 'order_date', - intervalLabel: '1m', - } as GroupByEntry, - ], - aggregationEntries: [ - { - identifier: 'avg(products.base_price)', - label: 'products.base_price.avg', - }, - ], - transformId: `ec_1_${Date.now()}`, - transformDescription: - 'ecommerce batch transform with groups terms(category.keyword) + date_histogram(order_date) 1m and aggregation avg(products.base_price)', - get destinationIndex(): string { - return `dest_${this.transformId}`; - }, - expected: { - row: { - status: 'stopped', - mode: 'batch', - progress: '100', - }, - }, - }, - ]; - - for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function() { - after(async () => { - await transform.api.deleteIndices(testData.destinationIndex); - }); - - it('loads the home page', async () => { - await transform.navigation.navigateTo(); - await transform.management.assertTransformListPageExists(); - }); - - it('displays the stats bar', async () => { - await transform.management.assertTransformStatsBarExists(); - }); - - it('loads the source selection modal', async () => { - await transform.management.startTransformCreation(); - }); - - it('selects the source data', async () => { - await transform.sourceSelection.selectSource(testData.source); - }); - - it('displays the define pivot step', async () => { - await transform.wizard.assertDefineStepActive(); - }); - - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); - }); - - it('displays an empty pivot preview', async () => { - await transform.wizard.assertPivotPreviewEmpty(); - }); - - it('displays the query input', async () => { - await transform.wizard.assertQueryInputExists(); - await transform.wizard.assertQueryValue(''); - }); - - it('displays the advanced query editor switch', async () => { - await transform.wizard.assertAdvancedQueryEditorSwitchExists(); - await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); - }); - - it('adds the group by entries', async () => { - for (const [index, entry] of testData.groupByEntries.entries()) { - await transform.wizard.assertGroupByInputExists(); - await transform.wizard.assertGroupByInputValue([]); - await transform.wizard.addGroupByEntry( - index, - entry.identifier, - entry.label, - entry.intervalLabel - ); - } - }); - - it('adds the aggregation entries', async () => { - for (const [index, agg] of testData.aggregationEntries.entries()) { - await transform.wizard.assertAggregationInputExists(); - await transform.wizard.assertAggregationInputValue([]); - await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); - } - }); - - it('displays the advanced pivot editor switch', async () => { - await transform.wizard.assertAdvancedPivotEditorSwitchExists(); - await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); - }); - - it('loads the pivot preview', async () => { - await transform.wizard.assertPivotPreviewLoaded(); - }); - - it('loads the details step', async () => { - await transform.wizard.advanceToDetailsStep(); - }); - - it('inputs the transform id', async () => { - await transform.wizard.assertTransformIdInputExists(); - await transform.wizard.assertTransformIdValue(''); - await transform.wizard.setTransformId(testData.transformId); - }); - - it('inputs the transform description', async () => { - await transform.wizard.assertTransformDescriptionInputExists(); - await transform.wizard.assertTransformDescriptionValue(''); - await transform.wizard.setTransformDescription(testData.transformDescription); - }); - - it('inputs the destination index', async () => { - await transform.wizard.assertDestinationIndexInputExists(); - await transform.wizard.assertDestinationIndexValue(''); - await transform.wizard.setDestinationIndex(testData.destinationIndex); - }); - - it('displays the create index pattern switch', async () => { - await transform.wizard.assertCreateIndexPatternSwitchExists(); - await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); - }); - - it('displays the continuous mode switch', async () => { - await transform.wizard.assertContinuousModeSwitchExists(); - await transform.wizard.assertContinuousModeSwitchCheckState(false); - }); - - it('loads the create step', async () => { - await transform.wizard.advanceToCreateStep(); - }); - - it('displays the create and start button', async () => { - await transform.wizard.assertCreateAndStartButtonExists(); - }); - - it('displays the create button', async () => { - await transform.wizard.assertCreateButtonExists(); - }); - - it('displays the copy to clipboard button', async () => { - await transform.wizard.assertCreateAndStartButtonExists(); - }); - - it('creates the transform', async () => { - await transform.wizard.createTransform(); - }); - - it('starts the transform and finishes processing', async () => { - await transform.wizard.startTransform(); - await transform.wizard.waitForProgressBarComplete(); - }); - - it('returns to the management page', async () => { - await transform.wizard.returnToManagement(); - }); - - it('displays the transforms table', async () => { - await transform.management.assertTransformsTableExists(); - }); - - it('displays the created transform in the transform list', async () => { - await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1); - }); - - it('job creation displays details for the created job in the job list', async () => { - await transform.table.assertTransformRowFields(testData.transformId, { - id: testData.transformId, - description: testData.transformDescription, - sourceIndex: testData.source, - destinationIndex: testData.destinationIndex, - status: testData.expected.row.status, - mode: testData.expected.row.mode, - progress: testData.expected.row.progress, - }); - }); - }); - } - }); -} diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts new file mode 100644 index 0000000000000..3dbf61221abf9 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +interface GroupByEntry { + identifier: string; + label: string; + intervalLabel?: string; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('creation_index_pattern', function() { + this.tags(['smoke']); + before(async () => { + await esArchiver.load('ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await transform.api.cleanTransformIndices(); + }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms+date_histogram groups and avg agg', + source: 'ecommerce', + groupByEntries: [ + { + identifier: 'terms(category.keyword)', + label: 'category.keyword', + } as GroupByEntry, + { + identifier: 'date_histogram(order_date)', + label: 'order_date', + intervalLabel: '1m', + } as GroupByEntry, + ], + aggregationEntries: [ + { + identifier: 'avg(products.base_price)', + label: 'products.base_price.avg', + }, + ], + transformId: `ec_1_${Date.now()}`, + transformDescription: + 'ecommerce batch transform with groups terms(category.keyword) + date_histogram(order_date) 1m and aggregation avg(products.base_price)', + get destinationIndex(): string { + return `dest_${this.transformId}`; + }, + expected: { + pivotPreview: { + column: 0, + values: [`Men's Accessories`], + }, + row: { + status: 'stopped', + mode: 'batch', + progress: '100', + }, + sourcePreview: { + columns: 6, + rows: 5, + }, + }, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + await transform.api.deleteIndices(testData.destinationIndex); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + + it('displays the stats bar', async () => { + await transform.management.assertTransformStatsBarExists(); + }); + + it('loads the source selection modal', async () => { + await transform.management.startTransformCreation(); + }); + + it('selects the source data', async () => { + await transform.sourceSelection.selectSource(testData.source); + }); + + it('displays the define pivot step', async () => { + await transform.wizard.assertDefineStepActive(); + }); + + it('loads the source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewLoaded(); + }); + + it('shows the source index preview', async () => { + await transform.wizard.assertSourceIndexPreview( + testData.expected.sourcePreview.columns, + testData.expected.sourcePreview.rows + ); + }); + + it('displays an empty pivot preview', async () => { + await transform.wizard.assertPivotPreviewEmpty(); + }); + + it('displays the query input', async () => { + await transform.wizard.assertQueryInputExists(); + await transform.wizard.assertQueryValue(''); + }); + + it('displays the advanced query editor switch', async () => { + await transform.wizard.assertAdvancedQueryEditorSwitchExists(); + await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); + }); + + it('adds the group by entries', async () => { + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + }); + + it('adds the aggregation entries', async () => { + for (const [index, agg] of testData.aggregationEntries.entries()) { + await transform.wizard.assertAggregationInputExists(); + await transform.wizard.assertAggregationInputValue([]); + await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + } + }); + + it('displays the advanced pivot editor switch', async () => { + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); + }); + + it('loads the pivot preview', async () => { + await transform.wizard.assertPivotPreviewLoaded(); + }); + + it('shows the pivot preview', async () => { + await transform.wizard.assertPivotPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + + it('loads the details step', async () => { + await transform.wizard.advanceToDetailsStep(); + }); + + it('inputs the transform id', async () => { + await transform.wizard.assertTransformIdInputExists(); + await transform.wizard.assertTransformIdValue(''); + await transform.wizard.setTransformId(testData.transformId); + }); + + it('inputs the transform description', async () => { + await transform.wizard.assertTransformDescriptionInputExists(); + await transform.wizard.assertTransformDescriptionValue(''); + await transform.wizard.setTransformDescription(testData.transformDescription); + }); + + it('inputs the destination index', async () => { + await transform.wizard.assertDestinationIndexInputExists(); + await transform.wizard.assertDestinationIndexValue(''); + await transform.wizard.setDestinationIndex(testData.destinationIndex); + }); + + it('displays the create index pattern switch', async () => { + await transform.wizard.assertCreateIndexPatternSwitchExists(); + await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); + }); + + it('displays the continuous mode switch', async () => { + await transform.wizard.assertContinuousModeSwitchExists(); + await transform.wizard.assertContinuousModeSwitchCheckState(false); + }); + + it('loads the create step', async () => { + await transform.wizard.advanceToCreateStep(); + }); + + it('displays the create and start button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('displays the create button', async () => { + await transform.wizard.assertCreateButtonExists(); + }); + + it('displays the copy to clipboard button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('creates the transform', async () => { + await transform.wizard.createTransform(); + }); + + it('starts the transform and finishes processing', async () => { + await transform.wizard.startTransform(); + await transform.wizard.waitForProgressBarComplete(); + }); + + it('returns to the management page', async () => { + await transform.wizard.returnToManagement(); + }); + + it('displays the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('displays the created transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.transformId); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await transform.table.assertTransformRowFields(testData.transformId, { + id: testData.transformId, + description: testData.transformDescription, + sourceIndex: testData.source, + destinationIndex: testData.destinationIndex, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts new file mode 100644 index 0000000000000..8a69700bee578 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +interface GroupByEntry { + identifier: string; + label: string; + intervalLabel?: string; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('creation_saved_search', function() { + this.tags(['smoke']); + before(async () => { + await esArchiver.load('ml/farequote'); + }); + + // after(async () => { + // await esArchiver.unload('ml/farequote'); + // await transform.api.cleanTransformIndices(); + // }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms groups and avg agg with saved search filter', + source: 'farequote_filter', + groupByEntries: [ + { + identifier: 'terms(airline)', + label: 'airline', + } as GroupByEntry, + ], + aggregationEntries: [ + { + identifier: 'avg(responsetime)', + label: 'responsetime.avg', + }, + ], + transformId: `fq_1_${Date.now()}`, + transformDescription: + 'farequote batch transform with groups terms(airline) and aggregation avg(responsetime.avg) with saved search filter', + get destinationIndex(): string { + return `dest_${this.transformId}`; + }, + expected: { + pivotPreview: { + column: 0, + values: ['ASA'], + }, + row: { + status: 'stopped', + mode: 'batch', + progress: '100', + }, + sourceIndex: 'farequote', + sourcePreview: { + column: 3, + values: ['ASA'], + }, + }, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + await transform.api.deleteIndices(testData.destinationIndex); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + + it('displays the stats bar', async () => { + await transform.management.assertTransformStatsBarExists(); + }); + + it('loads the source selection modal', async () => { + await transform.management.startTransformCreation(); + }); + + it('selects the source data', async () => { + await transform.sourceSelection.selectSource(testData.source); + }); + + it('displays the define pivot step', async () => { + await transform.wizard.assertDefineStepActive(); + }); + + it('loads the source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewLoaded(); + }); + + it('shows the filtered source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewColumnValues( + testData.expected.sourcePreview.column, + testData.expected.sourcePreview.values + ); + }); + + it('displays an empty pivot preview', async () => { + await transform.wizard.assertPivotPreviewEmpty(); + }); + + it('hides the query input', async () => { + await transform.wizard.assertQueryInputMissing(); + }); + + it('hides the advanced query editor switch', async () => { + await transform.wizard.assertAdvancedQueryEditorSwitchMissing(); + }); + + it('adds the group by entries', async () => { + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + }); + + it('adds the aggregation entries', async () => { + for (const [index, agg] of testData.aggregationEntries.entries()) { + await transform.wizard.assertAggregationInputExists(); + await transform.wizard.assertAggregationInputValue([]); + await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + } + }); + + it('displays the advanced pivot editor switch', async () => { + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); + }); + + it('loads the pivot preview', async () => { + await transform.wizard.assertPivotPreviewLoaded(); + }); + + it('shows the pivot preview', async () => { + await transform.wizard.assertPivotPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + + it('loads the details step', async () => { + await transform.wizard.advanceToDetailsStep(); + }); + + it('inputs the transform id', async () => { + await transform.wizard.assertTransformIdInputExists(); + await transform.wizard.assertTransformIdValue(''); + await transform.wizard.setTransformId(testData.transformId); + }); + + it('inputs the transform description', async () => { + await transform.wizard.assertTransformDescriptionInputExists(); + await transform.wizard.assertTransformDescriptionValue(''); + await transform.wizard.setTransformDescription(testData.transformDescription); + }); + + it('inputs the destination index', async () => { + await transform.wizard.assertDestinationIndexInputExists(); + await transform.wizard.assertDestinationIndexValue(''); + await transform.wizard.setDestinationIndex(testData.destinationIndex); + }); + + it('displays the create index pattern switch', async () => { + await transform.wizard.assertCreateIndexPatternSwitchExists(); + await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); + }); + + it('displays the continuous mode switch', async () => { + await transform.wizard.assertContinuousModeSwitchExists(); + await transform.wizard.assertContinuousModeSwitchCheckState(false); + }); + + it('loads the create step', async () => { + await transform.wizard.advanceToCreateStep(); + }); + + it('displays the create and start button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('displays the create button', async () => { + await transform.wizard.assertCreateButtonExists(); + }); + + it('displays the copy to clipboard button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('creates the transform', async () => { + await transform.wizard.createTransform(); + }); + + it('starts the transform and finishes processing', async () => { + await transform.wizard.startTransform(); + await transform.wizard.waitForProgressBarComplete(); + }); + + it('returns to the management page', async () => { + await transform.wizard.returnToManagement(); + }); + + it('displays the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('displays the created transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.transformId); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await transform.table.assertTransformRowFields(testData.transformId, { + id: testData.transformId, + description: testData.transformDescription, + sourceIndex: testData.expected.sourceIndex, + destinationIndex: testData.destinationIndex, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + }); + + it('expands the transform management table row and walks through available tabs', async () => { + await transform.table.assertTransformExpandedRow(); + }); + + it('displays the transform preview in the expanded row', async () => { + await transform.table.assertTransformsExpandedRowPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index adee997905a31..0a33ce0ebf08a 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -9,6 +9,7 @@ export default function({ loadTestFile }: FtrProviderContext) { describe('transform', function() { this.tags(['ciGroup9', 'transform']); - loadTestFile(require.resolve('./creation')); + loadTestFile(require.resolve('./creation_index_pattern')); + loadTestFile(require.resolve('./creation_saved_search')); }); } diff --git a/x-pack/test/functional/apps/uptime/monitor.ts b/x-pack/test/functional/apps/uptime/monitor.ts index ecbe893b1f2c0..034ccad4815a1 100644 --- a/x-pack/test/functional/apps/uptime/monitor.ts +++ b/x-pack/test/functional/apps/uptime/monitor.ts @@ -19,8 +19,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => await esArchiver.unload(archive)); it('loads and displays uptime data based on date range', async () => { await pageObjects.uptime.loadDataAndGoToMonitorPage( - '2019-09-10 12:40:08.078', - '2019-09-11 19:40:08.078', + 'Sep 10, 2019 @ 12:40:08.078', + 'Sep 11, 2019 @ 19:40:08.078', '0000-intermittent', '0000-intermittent' ); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index cdb904537a0f2..9a337c86185fe 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -12,8 +12,8 @@ export default ({ getPageObjects }: FtrProviderContext) => { const pageObjects = getPageObjects(['uptime']); describe('overview page', function() { - const DEFAULT_DATE_START = '2019-09-10 12:40:08.078'; - const DEFAULT_DATE_END = '2019-09-11 19:40:08.078'; + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; it('loads and displays uptime data based on date range', async () => { await pageObjects.uptime.goToUptimeOverviewAndLoadData( DEFAULT_DATE_START, diff --git a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts index dacd0b75b126c..03b6ed8e8e7c5 100644 --- a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts +++ b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts @@ -75,8 +75,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { ['2019-08-22 00:00', 'php', '11'], ['2019-08-22 16:00', 'jpg', '3'], ]; - const fromTime = '2019-08-19 01:55:07.240'; - const toTime = '2019-08-22 23:09:36.205'; + const fromTime = 'Aug 19, 2019 @ 01:55:07.240'; + const toTime = 'Aug 22, 2019 @ 23:09:36.205'; await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.clickVisualizationByName('hybrid_histogram_line_chart'); diff --git a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json index 18b359d37aaa6..5256e29956f4f 100644 --- a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json @@ -99,7 +99,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1068,4 +1068,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/basic/mappings.json b/x-pack/test/functional/es_archives/lens/basic/mappings.json index b87dbe12a7005..f2a29f022ff5e 100644 --- a/x-pack/test/functional/es_archives/lens/basic/mappings.json +++ b/x-pack/test/functional/es_archives/lens/basic/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1291,4 +1291,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 0321d57bc2df6..8b8e5a0e6e7f6 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1300,4 +1300,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index 4fe559cc85fe1..b00545c015a74 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -133,7 +133,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json index 34420b6bb63e1..dd717387a2643 100644 --- a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json @@ -84,7 +84,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1091,4 +1091,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 8153a8713ca2f..480814cb02781 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -46,7 +46,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Move the date filter to the specified time range, defaults to * a range that has data in our dataset. */ - goToTimeRange(fromTime = '2015-09-19 06:31:44.000', toTime = '2015-09-23 18:31:44.000') { + goToTimeRange(fromTime?: string, toTime?: string) { + fromTime = fromTime || PageObjects.timePicker.defaultStartTime; + toTime = toTime || PageObjects.timePicker.defaultEndTime; return PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }, diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index 30de10c400c88..0ba0325ad5602 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -165,15 +165,13 @@ export function ReportingPageProvider({ getService, getPageObjects }) { async setTimepickerInDataRange() { log.debug('Reporting:setTimepickerInDataRange'); - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setDefaultAbsoluteRange(); } async setTimepickerInNoDataRange() { log.debug('Reporting:setTimepickerInNoDataRange'); - const fromTime = '1999-09-19 06:31:44.000'; - const toTime = '1999-09-23 18:31:44.000'; + const fromTime = 'Sep 19, 1999 @ 06:31:44.000'; + const toTime = 'Sep 23, 1999 @ 18:31:44.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } } diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 270722a97d6b6..2fc027a81ea8c 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); const esSupertest = getService('esSupertest'); diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index ddab5fd68f13c..5ffb235a828d6 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -15,7 +15,6 @@ export function MachineLearningJobManagementProvider( mlApi: ProvidedType ) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); return { async navigateToNewJobSourceSelection() { @@ -36,10 +35,7 @@ export function MachineLearningJobManagementProvider( }, async assertStartDatafeedModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 5000 }); }, async confirmStartDatafeedModal() { diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts index 71b76a6885592..3e7dacb23d61b 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -146,10 +146,7 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async assertCreateDetectorModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlCreateDetectorModal'); - }); + await testSubjects.existOrFail('mlCreateDetectorModal', { timeout: 5000 }); }, async assertDetectorFunctionInputExists() { @@ -298,18 +295,17 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async clickEditDetector(detectorIndex: number) { - await testSubjects.click( - `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` - ); - await this.assertCreateDetectorModalExists(); + await retry.tryForTime(20 * 1000, async () => { + await testSubjects.click( + `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` + ); + await this.assertCreateDetectorModalExists(); + }); }, async createJob() { await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 10 * 1000 }); }, }; } diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts index 9050b5944dee3..a6756e5940d72 100644 --- a/x-pack/test/functional/services/transform_ui/api.ts +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformAPIProvider({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); diff --git a/x-pack/test/functional/services/transform_ui/transform_table.ts b/x-pack/test/functional/services/transform_ui/transform_table.ts index b9eff5e2b2435..ebd7fe527b45f 100644 --- a/x-pack/test/functional/services/transform_ui/transform_table.ts +++ b/x-pack/test/functional/services/transform_ui/transform_table.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformTableProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return new (class TransformTable { @@ -60,6 +61,51 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { return rows; } + async parseEuiInMemoryTable(tableSubj: string) { + const table = await testSubjects.find(`~${tableSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + // For each row, get the content of each cell and + // add its values as an array to each row. + for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + rows.push( + $(tr) + .find('.euiTableCellContent') + .toArray() + .map(cell => + $(cell) + .text() + .trim() + ) + ); + } + + return rows; + } + + async assertEuiInMemoryTableColumnValues( + tableSubj: string, + column: number, + expectedColumnValues: string[] + ) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rows = await this.parseEuiInMemoryTable(tableSubj); + + // reduce the rows data to an array of unique values in the specified column + const uniqueColumnValues = rows + .map(row => row[column]) + .flat() + .filter((v, i, a) => a.indexOf(v) === i); + + uniqueColumnValues.sort(); + + // check if the returned unique value matches the supplied filter value + expect(uniqueColumnValues).to.eql(expectedColumnValues); + }); + } + public async refreshTransformList() { await testSubjects.click('transformRefreshTransformListButton'); await this.waitForTransformsToLoad(); @@ -83,5 +129,36 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { const transformRow = rows.filter(row => row.id === transformId)[0]; expect(transformRow).to.eql(expectedRow); } + + public async assertTransformExpandedRow() { + await testSubjects.click('transformListRowDetailsToggle'); + + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab'); + await testSubjects.existOrFail('~transformDetailsTabContent'); + + // Walk through the rest of the tabs and check if the corresponding content shows up + await testSubjects.existOrFail('transformJsonTab'); + await testSubjects.click('transformJsonTab'); + await testSubjects.existOrFail('~transformJsonTabContent'); + + await testSubjects.existOrFail('transformMessagesTab'); + await testSubjects.click('transformMessagesTab'); + await testSubjects.existOrFail('~transformMessagesTabContent'); + + await testSubjects.existOrFail('transformPreviewTab'); + await testSubjects.click('transformPreviewTab'); + await testSubjects.existOrFail('~transformPreviewTabContent'); + } + + public async waitForTransformsExpandedRowPreviewTabToLoad() { + await testSubjects.existOrFail('~transformPreviewTabContent', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('transformPreviewTabContent loaded', { timeout: 30 * 1000 }); + } + + async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) { + await this.waitForTransformsExpandedRowPreviewTabToLoad(); + await this.assertEuiInMemoryTableColumnValues('transformPreviewTabContent', column, values); + } })(); } diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index c80aa62cd4912..db7cdd148fd99 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -75,6 +75,81 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, + async parseEuiInMemoryTable(tableSubj: string) { + const table = await testSubjects.find(`~${tableSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + // For each row, get the content of each cell and + // add its values as an array to each row. + for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + rows.push( + $(tr) + .find('.euiTableCellContent') + .toArray() + .map(cell => + $(cell) + .text() + .trim() + ) + ); + } + + return rows; + }, + + async assertEuiInMemoryTableColumnValues( + tableSubj: string, + column: number, + expectedColumnValues: string[] + ) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rows = await this.parseEuiInMemoryTable(tableSubj); + + // reduce the rows data to an array of unique values in the specified column + const uniqueColumnValues = rows + .map(row => row[column]) + .flat() + .filter((v, i, a) => a.indexOf(v) === i); + + uniqueColumnValues.sort(); + + // check if the returned unique value matches the supplied filter value + expect(uniqueColumnValues).to.eql( + expectedColumnValues, + `Unique EuiInMemoryTable column values should be '${expectedColumnValues.join()}' (got ${uniqueColumnValues.join()})` + ); + }); + }, + + async assertSourceIndexPreview(columns: number, rows: number) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rowsData = await this.parseEuiInMemoryTable('transformSourceIndexPreview'); + + expect(rowsData).to.length( + rows, + `EuiInMemoryTable rows should be ${rows} (got ${rowsData.length})` + ); + + rowsData.map((r, i) => + expect(r).to.length( + columns, + `EuiInMemoryTable row #${i + 1} column count should be ${columns} (got ${r.length})` + ) + ); + }); + }, + + async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiInMemoryTableColumnValues('transformSourceIndexPreview', column, values); + }, + + async assertPivotPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiInMemoryTableColumnValues('transformPivotPreview', column, values); + }, + async assertPivotPreviewLoaded() { await this.assertPivotPreviewExists('loaded'); }, @@ -87,6 +162,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('tarnsformQueryInput'); }, + async assertQueryInputMissing() { + await testSubjects.missingOrFail('tarnsformQueryInput'); + }, + async assertQueryValue(expectedQuery: string) { const actualQuery = await testSubjects.getVisibleText('tarnsformQueryInput'); expect(actualQuery).to.eql( @@ -99,6 +178,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(`transformAdvancedQueryEditorSwitch`, { allowHidden: true }); }, + async assertAdvancedQueryEditorSwitchMissing() { + await testSubjects.missingOrFail(`transformAdvancedQueryEditorSwitch`); + }, + async assertAdvancedQueryEditorSwitchCheckState(expectedCheckState: boolean) { const actualCheckState = (await testSubjects.getAttribute('transformAdvancedQueryEditorSwitch', 'aria-checked')) === diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 0a3b7f595c66d..450f7b1a427dc 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -362,7 +362,7 @@ export default function({ getService }: FtrProviderContext) { // Let's delete tokens from `.security-tokens` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index after // some period of time. - const esResponse = await getService('es').deleteByQuery({ + const esResponse = await getService('legacyEs').deleteByQuery({ index: '.security-tokens', q: 'doc_type:token', refresh: true, diff --git a/x-pack/test/kerberos_api_integration/services.ts b/x-pack/test/kerberos_api_integration/services.ts index 42505994e0602..e2e7a5b8844a7 100644 --- a/x-pack/test/kerberos_api_integration/services.ts +++ b/x-pack/test/kerberos_api_integration/services.ts @@ -7,7 +7,7 @@ import { services as apiIntegrationServices } from '../api_integration/services'; export const services = { - es: apiIntegrationServices.es, + legacyEs: apiIntegrationServices.legacyEs, esSupertest: apiIntegrationServices.esSupertest, supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, }; diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js index 7460b1f9e238c..80ef6bd6df4ff 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js @@ -482,7 +482,7 @@ export default function ({ getService }) { // Let's delete tokens from `.security-tokens` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. - const esResponse = await getService('es').deleteByQuery({ + const esResponse = await getService('legacyEs').deleteByQuery({ index: '.security-tokens', q: 'doc_type:token', refresh: true, diff --git a/x-pack/test/oidc_api_integration/services.ts b/x-pack/test/oidc_api_integration/services.ts index e4ff6048a8cce..dda9f1d1a8886 100644 --- a/x-pack/test/oidc_api_integration/services.ts +++ b/x-pack/test/oidc_api_integration/services.ts @@ -7,6 +7,6 @@ import { services as apiIntegrationServices } from '../api_integration/services'; export const services = { - es: apiIntegrationServices.es, + legacyEs: apiIntegrationServices.legacyEs, supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, }; diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js index 938324c12a377..73253224bb45d 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +const { EventEmitter } = require('events'); + import { initRoutes } from './init_routes'; + +const once = function (emitter, event) { + return new Promise(resolve => { + emitter.once(event, resolve); + }); +}; + export default function TaskTestingAPI(kibana) { + const taskTestingEvents = new EventEmitter(); + return new kibana.Plugin({ name: 'sampleTask', require: ['elasticsearch', 'task_manager'], @@ -52,6 +63,10 @@ export default function TaskTestingAPI(kibana) { refresh: true, }); + if (params.waitForEvent) { + await once(taskTestingEvents, params.waitForEvent); + } + return { state: { count: (prevState.count || 0) + 1 }, runAt: millisecondsFromNow(params.nextRunMilliseconds), @@ -88,7 +103,7 @@ export default function TaskTestingAPI(kibana) { }, }); - initRoutes(server); + initRoutes(server, taskTestingEvents); }, }); } diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index a9dfabae6d609..7b9e265a15d6f 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -23,11 +23,44 @@ const taskManagerQuery = { } }; -export function initRoutes(server) { +export function initRoutes(server, taskTestingEvents) { const taskManager = server.plugins.task_manager; server.route({ - path: '/api/sample_tasks', + path: '/api/sample_tasks/schedule', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + task: Joi.object({ + taskType: Joi.string().required(), + interval: Joi.string().optional(), + params: Joi.object().required(), + state: Joi.object().optional(), + id: Joi.string().optional() + }) + }), + }, + }, + async handler(request) { + try { + const { task: taskFields } = request.payload; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await (taskManager.schedule(task, { request })); + + return taskResult; + } catch (err) { + return err; + } + }, + }); + + server.route({ + path: '/api/sample_tasks/ensure_scheduled', method: 'POST', config: { validate: { @@ -38,26 +71,19 @@ export function initRoutes(server) { params: Joi.object().required(), state: Joi.object().optional(), id: Joi.string().optional() - }), - ensureScheduled: Joi.boolean() - .default(false) - .optional(), + }) }), }, }, async handler(request) { try { - const { ensureScheduled = false, task: taskFields } = request.payload; + const { task: taskFields } = request.payload; const task = { ...taskFields, scope: [scope], }; - const taskResult = await ( - ensureScheduled - ? taskManager.ensureScheduled(task, { request }) - : taskManager.schedule(task, { request }) - ); + const taskResult = await (taskManager.ensureScheduled(task, { request })); return taskResult; } catch (err) { @@ -66,6 +92,27 @@ export function initRoutes(server) { }, }); + server.route({ + path: '/api/sample_tasks/event', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + event: Joi.string().required() + }), + }, + }, + async handler(request) { + try { + const { event } = request.payload; + taskTestingEvents.emit(event); + return { event }; + } catch (err) { + return err; + } + }, + }); + server.route({ path: '/api/sample_tasks', method: 'GET', diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts b/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts index 98d653d71b5ec..ab9f7d2cdd339 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts +++ b/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts @@ -9,7 +9,7 @@ import { SavedObject } from 'src/core/server'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const randomness = getService('randomness'); const supertest = getService('supertest'); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index a8ff03dc71d24..986648f795da6 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -12,7 +12,7 @@ import supertestAsPromised from 'supertest-as-promised'; const { task: { properties: taskManagerIndexMapping } } = require('../../../../legacy/plugins/task_manager/mappings.json'); export default function ({ getService }) { - const es = getService('es'); + const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); const config = getService('config'); @@ -58,7 +58,7 @@ export default function ({ getService }) { } function scheduleTask(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/schedule') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) @@ -66,13 +66,20 @@ export default function ({ getService }) { } function scheduleTaskIfNotExists(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/ensure_scheduled') .set('kbn-xsrf', 'xxx') - .send({ task, ensureScheduled: true }) + .send({ task }) .expect(200) .then((response) => response.body); } + function releaseTasksWaitingForEventToComplete(event) { + return supertest.post('/api/sample_tasks/event') + .set('kbn-xsrf', 'xxx') + .send({ event }) + .expect(200); + } + it('should support middleware', async () => { const historyItem = _.random(1, 100); @@ -204,5 +211,45 @@ export default function ({ getService }) { expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.greaterThan(expectedDiff - buffer); expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); } + + it('should run tasks in parallel, allowing for long running tasks along side faster tasks', async () => { + /** + * It's worth noting this test relies on the /event endpoint that forces Task Manager to hold off + * on completing a task until a call is made by the test suite. + * If we begin testing with multiple Kibana instacnes in Parallel this will likely become flaky. + * If you end up here because the test is flaky, this might be why. + */ + const fastTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { }, + }); + + const longRunningTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { + waitForEvent: 'rescheduleHasHappened' + }, + }); + + function getTaskById(tasks, id) { + return tasks.filter(task => task.id === id)[0]; + } + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(getTaskById(tasks, fastTask.id).state.count).to.eql(2); + }); + + await releaseTasksWaitingForEventToComplete('rescheduleHasHappened'); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + + expect(getTaskById(tasks, fastTask.id).state.count).to.greaterThan(2); + expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); + }); + }); }); } diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 276b42423860c..d512894fbb1f2 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -622,7 +622,7 @@ export default function({ getService }: FtrProviderContext) { // Let's delete tokens from `.security` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. - const esResponse = await getService('es').deleteByQuery({ + const esResponse = await getService('legacyEs').deleteByQuery({ index: '.security-tokens', q: 'doc_type:token', refresh: true, diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index 9d3029db013d3..6ea29b0d9e56e 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -21,7 +21,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { servers: xPackAPITestsConfig.get('servers'), services: { randomness: kibanaAPITestsConfig.get('services.randomness'), - es: kibanaAPITestsConfig.get('services.es'), + legacyEs: kibanaAPITestsConfig.get('services.legacyEs'), supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'), }, junit: { diff --git a/x-pack/test/saml_api_integration/services.ts b/x-pack/test/saml_api_integration/services.ts index 307108fa097f4..d207112f6e81f 100644 --- a/x-pack/test/saml_api_integration/services.ts +++ b/x-pack/test/saml_api_integration/services.ts @@ -8,6 +8,6 @@ import { services as apiIntegrationServices } from '../api_integration/services' export const services = { randomness: apiIntegrationServices.randomness, - es: apiIntegrationServices.es, + legacyEs: apiIntegrationServices.legacyEs, supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, }; diff --git a/x-pack/test/saved_object_api_integration/common/services/index.ts b/x-pack/test/saved_object_api_integration/common/services/index.ts index 1ea312714fc1e..aba605fee8947 100644 --- a/x-pack/test/saved_object_api_integration/common/services/index.ts +++ b/x-pack/test/saved_object_api_integration/common/services/index.ts @@ -12,7 +12,7 @@ import { services as kibanaApiIntegrationServices } from '../../../../../test/ap import { services as kibanaFunctionalServices } from '../../../../../test/functional/services'; export const services = { - es: LegacyEsProvider, + legacyEs: LegacyEsProvider, esSupertestWithoutAuth: apiIntegrationServices.esSupertestWithoutAuth, supertest: kibanaApiIntegrationServices.supertest, supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 7e00d0a9d7f75..7768665f3b941 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -12,7 +12,7 @@ import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { bulkCreateTest, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index 4cc94bf21f235..e4adaa580c1db 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -11,7 +11,7 @@ import { createTestSuiteFactory } from '../../common/suites/create'; export default function({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const { diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index f9048d1493cc6..58859c292ce35 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -12,7 +12,7 @@ import { importTestSuiteFactory } from '../../common/suites/import'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { importTest, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 1b1e9762a54e5..bb42c5422ece5 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -8,7 +8,7 @@ import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function() { diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index fa070ac76ce57..6c91fe6310170 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -12,7 +12,7 @@ import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { resolveImportErrorsTest, diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 2a5757fb0cc57..943a22c4399c7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -11,7 +11,7 @@ import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { bulkCreateTest, diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index bb2bf7e0e0d7a..60a9fa0a86aa6 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -10,7 +10,7 @@ import { createTestSuiteFactory } from '../../common/suites/create'; export default function({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const { diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 8ad7e070edf54..770410dcfed81 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -11,7 +11,7 @@ import { importTestSuiteFactory } from '../../common/suites/import'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { importTest, diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index 6e930338a1184..bb637a9bc4c90 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -8,7 +8,7 @@ import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); describe('saved objects security only enabled', function() { diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index e1d72f8641e93..59d50c16c259a 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -11,7 +11,7 @@ import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { resolveImportErrorsTest, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 6b67c63ed349a..d89e3b4f8605b 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -25,7 +25,7 @@ const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { bulkCreateTest, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index d6bd0b497759f..cf34f7505cdb2 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -23,7 +23,7 @@ const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { export default function({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const { diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index c8b2e68291dc3..c78a0e1cc2cce 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -11,7 +11,7 @@ import { importTestSuiteFactory } from '../../common/suites/import'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { importTest, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index 93d60ebbf4d6d..22a7ab81e5530 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -11,7 +11,7 @@ import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { resolveImportErrorsTest, diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 23514c635d356..dffc6c524cc6e 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -34,7 +34,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) testFiles: [require.resolve(`../${name}/apis/`)], servers: config.xpack.api.get('servers'), services: { - es: LegacyEsProvider, + legacyEs: LegacyEsProvider, esSupertestWithoutAuth: config.xpack.api.get('services.esSupertestWithoutAuth'), supertest: config.kibana.api.get('services.supertest'), supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index c6c41f7cd3a97..1c4ca2fe20662 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -13,7 +13,7 @@ import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestInvoker) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { copyToSpaceTest, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts index 25d7317d7dd90..df4a2f51b4872 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts @@ -13,7 +13,7 @@ import { deleteTestSuiteFactory } from '../../common/suites/delete'; export default function deleteSpaceTestSuite({ getService }: TestInvoker) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { deleteTest, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 300949f41f036..e918ab0b53841 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -9,7 +9,7 @@ import { TestInvoker } from '../../common/lib/types'; // eslint-disable-next-line import/no-default-export export default function({ loadTestFile, getService }: TestInvoker) { - const es = getService('es'); + const es = getService('legacyEs'); const supertest = getService('supertest'); describe('spaces api with security', function() { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index b1427a9b77be0..4781c4bea9b30 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -11,7 +11,7 @@ import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { copyToSpaceTest, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts index 350b682429778..64cbb7a02776c 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts @@ -12,7 +12,7 @@ import { deleteTestSuiteFactory } from '../../common/suites/delete'; export default function deleteSpaceTestSuite({ getService }: TestInvoker) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); const { deleteTest, diff --git a/x-pack/test/token_api_integration/auth/header.js b/x-pack/test/token_api_integration/auth/header.js index 2317f62e63809..4b27fd5db3166 100644 --- a/x-pack/test/token_api_integration/auth/header.js +++ b/x-pack/test/token_api_integration/auth/header.js @@ -6,7 +6,7 @@ export default function ({ getService }) { const supertest = getService('supertestWithoutAuth'); - const es = getService('es'); + const es = getService('legacyEs'); async function createToken() { const { access_token: accessToken } = await es.shield.getAccessToken({ diff --git a/x-pack/test/token_api_integration/auth/session.js b/x-pack/test/token_api_integration/auth/session.js index 6045021894823..8a9f1d7a3f229 100644 --- a/x-pack/test/token_api_integration/auth/session.js +++ b/x-pack/test/token_api_integration/auth/session.js @@ -132,7 +132,7 @@ export default function ({ getService }) { // Let's delete tokens from `.security` index directly to simulate the case when // Elasticsearch automatically removes access/refresh token document from the index // after some period of time. - const esResponse = await getService('es').deleteByQuery({ + const esResponse = await getService('legacyEs').deleteByQuery({ index: '.security-tokens', q: 'doc_type:token', refresh: true, diff --git a/x-pack/test/token_api_integration/config.js b/x-pack/test/token_api_integration/config.js index 44086245a90c0..8cf9bc329a374 100644 --- a/x-pack/test/token_api_integration/config.js +++ b/x-pack/test/token_api_integration/config.js @@ -11,7 +11,7 @@ export default async function ({ readConfigFile }) { testFiles: [require.resolve('./auth')], servers: xPackAPITestsConfig.get('servers'), services: { - es: xPackAPITestsConfig.get('services.es'), + legacyEs: xPackAPITestsConfig.get('services.legacyEs'), supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'), }, junit: { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index a22ce122b1766..e309d0e127dfc 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,5 +9,5 @@ "include": [ "**/*" ], - "exclude": [], + "exclude": [] } diff --git a/x-pack/test/upgrade_assistant_integration/config.js b/x-pack/test/upgrade_assistant_integration/config.js index 4cd4ca78d3a58..3a43d43543b61 100644 --- a/x-pack/test/upgrade_assistant_integration/config.js +++ b/x-pack/test/upgrade_assistant_integration/config.js @@ -20,9 +20,9 @@ export default async function ({ readConfigFile }) { testFiles: [require.resolve('./upgrade_assistant')], servers: xPackFunctionalTestsConfig.get('servers'), services: { + ...kibanaCommonConfig.get('services'), supertest: kibanaAPITestsConfig.get('services.supertest'), - es: LegacyEsProvider, - esArchiver: kibanaCommonConfig.get('services.esArchiver'), + legacyEs: LegacyEsProvider, }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 233336c722611..137de18fc98d9 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -12,7 +12,7 @@ import { ReindexStatus, REINDEX_OP_TYPE } from '../../../legacy/plugins/upgrade_ export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const es = getService('es'); + const es = getService('legacyEs'); // Utility function that keeps polling API until reindex operation has completed or failed. const waitForReindexToComplete = async (indexName) => { diff --git a/x-pack/typings/@elastic/eui/index.d.ts b/x-pack/typings/@elastic/eui/index.d.ts index 93d90ed43ff4b..de9697f859fd7 100644 --- a/x-pack/typings/@elastic/eui/index.d.ts +++ b/x-pack/typings/@elastic/eui/index.d.ts @@ -7,8 +7,8 @@ // TODO: Remove once typescript definitions are in EUI declare module '@elastic/eui' { - export const EuiDescribedFormGroup: React.SFC; - export const EuiCodeEditor: React.SFC; + export const EuiDescribedFormGroup: React.FC; + export const EuiCodeEditor: React.FC; export const Query: any; } diff --git a/x-pack/typings/jest_styled_components.d.ts b/x-pack/typings/jest_styled_components.d.ts new file mode 100644 index 0000000000000..86f82ffb013c7 --- /dev/null +++ b/x-pack/typings/jest_styled_components.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// https://github.com/styled-components/jest-styled-components/issues/264 + +declare namespace jest { + interface AsymmetricMatcher { + $$typeof: Symbol; //eslint-disable-line + sample?: string | RegExp | object | Array | Function; // eslint-disable-line + } + + type Value = string | number | RegExp | AsymmetricMatcher | undefined; + + interface Options { + media?: string; + modifier?: string; + supports?: string; + } + + interface Matchers { + toHaveStyleRule(property: string, value?: Value, options?: Options): R; + } +} diff --git a/yarn.lock b/yarn.lock index 150e7a98796fe..e12a0eb46c6cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,6 +87,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.6.3": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== + dependencies: + "@babel/types" "^7.6.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -304,6 +314,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== +"@babel/parser@^7.6.3": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -948,17 +963,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.3.2" -"@babel/register@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.5.5.tgz#40fe0d474c8c8587b28d6ae18a03eddad3dac3c1" - integrity sha512-pdd5nNR+g2qDkXZlW1yRCWFlNrAn2PPdnZUB72zjX4l1Vv4fMRRLwyf+n/idFCLI1UgVGboUU8oVziwTBiyNKQ== +"@babel/register@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.7.0.tgz#4e23ecf840296ef79c605baaa5c89e1a2426314b" + integrity sha512-HV3GJzTvSoyOMWGYn2TAh6uL6g+gqKTgEZ99Q3+X9UURT1VPT/WcU46R61XftIc5rXytcOHZ4Z0doDlsjPomIg== dependencies: - core-js "^3.0.0" find-cache-dir "^2.0.0" lodash "^4.17.13" - mkdirp "^0.5.1" + make-dir "^2.1.0" pirates "^4.0.0" - source-map-support "^0.5.9" + source-map-support "^0.5.16" "@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.4.2": version "7.5.5" @@ -968,6 +982,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.2" +"@babel/runtime-corejs2@^7.6.3": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.7.4.tgz#b9c2b1b5882762005785bc47740195a0ac780888" + integrity sha512-hKNcmHQbBSJFnZ82ewYtWDZ3fXkP/l1XcfRtm7c8gHPM/DMecJtFFBEp7KMLZTuHwwb7RfemHdsEnd7L916Z6A== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/runtime@7.0.0-beta.54": version "7.0.0-beta.54" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" @@ -983,14 +1005,14 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.3", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": +"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -1045,6 +1067,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.4.5": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.3" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" @@ -1063,6 +1100,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -1126,21 +1172,6 @@ ts-debounce "^1.0.0" uuid "^3.3.2" -"@elastic/ctags-langserver@^0.1.11": - version "0.1.11" - resolved "https://registry.yarnpkg.com/@elastic/ctags-langserver/-/ctags-langserver-0.1.11.tgz#e4725a6a763a2ff61fd02bbe2b42aa70db5d0f3f" - integrity sha512-ODNcD+zFmuhMm649/4fGQXD1msafvBaHKsk9PDXTjLrUMozrCWj99tGKqR5HcwrdkoMr/3YRBre2r4cyomtoBw== - dependencies: - "@elastic/lsp-extension" "^0.1.1" - "@elastic/node-ctags" "1.0.2" - commander "^2.11.0" - find-root "^1.1.0" - line-column "^1.0.2" - minimatch "3.0.4" - mz "^2.7.0" - parse-gitignore "1.0.1" - vscode-languageserver "^5.2.1" - "@elastic/elasticsearch@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.4.0.tgz#57f4066acf25e9d4e9b4f6376088433aae6f25d4" @@ -1188,10 +1219,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@14.9.0": - version "14.9.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-14.9.0.tgz#934ab8d51c56671635dc17ac20ec325f43ceda75" - integrity sha512-0ZztvfRO3SNgHtS8a+4i6CSG3Yc+C0Kodzc7obY5wkOzissrnbwLZdU79hU/H6DHYCt/zYDdGcrDp6BeD67RtQ== +"@elastic/eui@16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-16.0.0.tgz#511898bfbeba5ffea6ac96c077d1184d657a451d" + integrity sha512-i9t13PzrsfBUolMZ6n2X9aAYJ/wUI2NJduCQlDU4zrXfFrM1LFJ5/KSCcpgzI8VNDakeA3PTml+oqD7J0qGA3g== dependencies: "@types/lodash" "^4.14.116" "@types/numeral" "^0.0.25" @@ -1203,12 +1234,12 @@ lodash "^4.17.11" numeral "^2.0.6" prop-types "^15.6.0" - react-ace "^5.5.0" + react-ace "^7.0.5" react-beautiful-dnd "^10.1.0" react-focus-lock "^1.17.7" - react-input-autosize "^2.2.1" + react-input-autosize "^2.2.2" react-is "~16.3.0" - react-virtualized "^9.18.5" + react-virtualized "^9.21.2" resize-observer-polyfill "^1.5.0" tabbable "^3.0.0" uuid "^3.1.0" @@ -1240,41 +1271,6 @@ oppsy "2.x.x" pumpify "1.3.x" -"@elastic/javascript-typescript-langserver@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@elastic/javascript-typescript-langserver/-/javascript-typescript-langserver-0.3.3.tgz#950c3fd129bf3954c4931e36b5c21d64b7ea2bb4" - integrity sha512-rmHZlGFDFvUIIydr/xSo5p4M30KV00F7bu5TxJQAL/vn65mZMYQvuVyZz3T2sHe+MahuitATe9W0MwUnchOfoA== - dependencies: - "@elastic/lsp-extension" "^0.1.2" - chai "^4.0.1" - chai-as-promised "^7.0.0" - chalk "^2.2.0" - commander "^2.9.0" - fast-json-patch "^2.0.2" - glob "^7.1.1" - iterare "^1.1.2" - jaeger-client "^3.5.3" - lodash "^4.17.4" - mz "^2.6.0" - object-hash "^1.1.8" - opentracing "^0.14.0" - rxjs "^5.5.0" - semaphore-async-await "^1.5.1" - string-similarity "^2.0.0" - typescript "~3.3.3333" - vscode-jsonrpc "^4.0.0" - vscode-languageserver "^5.0.0" - vscode-languageserver-types "^3.0.3" - yarn "^1.12.3" - -"@elastic/lsp-extension@^0.1.1", "@elastic/lsp-extension@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@elastic/lsp-extension/-/lsp-extension-0.1.2.tgz#7356d951d272e833d02a81e13a0ef710f9474195" - integrity sha512-yDj5Ht5KCHDwBlgrlusmLtV/Yxa5z2f3vMSYbNFotoRMup8345/ZwlFp/zmyl04iFOVpT8ouB34+Ttpzbpd3vA== - dependencies: - vscode-languageserver "^5.2.1" - vscode-languageserver-types "^3.14.0" - "@elastic/makelogs@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-5.0.0.tgz#0064e9009c4e480d17195ab70d627bc07635540f" @@ -1304,11 +1300,6 @@ resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.0.0.tgz#4d325df333fe1319556bb4d54214098ada1171d4" integrity sha512-bbjbEyILPRTRt0xnda18OttLtlkJBPuXx3CjISUSn9jhWqHoFMzfOaZ73D5jxZE2SaFZUrJYfPpqXP6qqPufAQ== -"@elastic/node-ctags@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@elastic/node-ctags/-/node-ctags-1.0.2.tgz#447d7694a5598f9413fe2b6f356d56f64f612dfd" - integrity sha512-EHhJ0NPlCvYy+gbzBMU4/Z/55hftfdwlAG8JwOy7g0ITmH6rFPanEnzg1WL3/L+pp8OlYHyvDLwmyg0+06y8LQ== - "@elastic/numeral@2.3.3": version "2.3.3" resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.3.3.tgz#94d38a35bd315efa7a6918b22695128fc40a885e" @@ -1328,16 +1319,6 @@ resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd" integrity sha512-Nti5s2dplBPhSKRwJxG9JXTMOev4jVOWcnTJD1TOkJr1MUBYKVZcNcJtIVMSvahWGmP0B/UfO9q9lyRqdivkvQ== -"@emotion/cache@^10.0.15": - version "10.0.15" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.15.tgz#b81767b48015aae2689c60373992145c67b8de02" - integrity sha512-8VthgeKhlGeTXSW1JN7I14AnAaiFPbOrqNqg3dPoGCZ3bnMjkrmRU0zrx0BtBw9esBaPaQgDB9y0tVgAGT2Mrg== - dependencies: - "@emotion/sheet" "0.9.3" - "@emotion/stylis" "0.8.4" - "@emotion/utils" "0.11.2" - "@emotion/weak-memoize" "0.2.3" - "@emotion/cache@^10.0.17", "@emotion/cache@^10.0.9": version "10.0.19" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.19.tgz#d258d94d9c707dcadaf1558def968b86bb87ad71" @@ -1348,7 +1329,7 @@ "@emotion/utils" "0.11.2" "@emotion/weak-memoize" "0.2.4" -"@emotion/core@^10.0.14": +"@emotion/core@^10.0.14", "@emotion/core@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.22.tgz#2ac7bcf9b99a1979ab5b0a876fbf37ab0688b177" integrity sha512-7eoP6KQVUyOjAkE6y4fdlxbZRA4ILs7dqkkm6oZUJmihtHv0UBq98VgPirq9T8F9K2gKu0J/au/TpKryKMinaA== @@ -1360,27 +1341,6 @@ "@emotion/sheet" "0.9.3" "@emotion/utils" "0.11.2" -"@emotion/core@^10.0.9": - version "10.0.16" - resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.16.tgz#e43630b65c84e31e81f34db3286eab584b08cfaa" - integrity sha512-whbiiA7FfPreBY4BqWky2qRfAZvq+4dKQ1WNJuiYQwPCNmb0pEYDgNheSbZoNKtGTtfPaM28hBbZAKWD5EZXmQ== - dependencies: - "@babel/runtime" "^7.4.3" - "@emotion/cache" "^10.0.15" - "@emotion/css" "^10.0.14" - "@emotion/serialize" "^0.11.9" - "@emotion/sheet" "0.9.3" - "@emotion/utils" "0.11.2" - -"@emotion/css@^10.0.14": - version "10.0.14" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.14.tgz#95dacabdd0e22845d1a1b0b5968d9afa34011139" - integrity sha512-MozgPkBEWvorcdpqHZE5x1D/PLEHUitALQCQYt2wayf4UNhpgQs2tN0UwHYS4FMy5ROBH+0ALyCFVYJ/ywmwlg== - dependencies: - "@emotion/serialize" "^0.11.8" - "@emotion/utils" "0.11.2" - babel-plugin-emotion "^10.0.14" - "@emotion/css@^10.0.22", "@emotion/css@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.22.tgz#37b1abb6826759fe8ac0af0ac0034d27de6d1793" @@ -1400,7 +1360,7 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== -"@emotion/is-prop-valid@0.8.5": +"@emotion/is-prop-valid@0.8.5", "@emotion/is-prop-valid@^0.8.3": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983" integrity sha512-6ZODuZSFofbxSbcxwsFz+6ioPjb0ISJRRPLZ+WIbjcU2IMU0Io+RGQjjaTgOvNQl007KICBm7zXQaYQEC1r6Bg== @@ -1417,7 +1377,7 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== -"@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14": +"@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14", "@emotion/serialize@^0.11.9": version "0.11.14" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.14.tgz#56a6d8d04d837cc5b0126788b2134c51353c6488" integrity sha512-6hTsySIuQTbDbv00AnUO6O6Xafdwo5GswRlMZ5hHqiFx+4pZ7uGWXUQFW46Kc2taGhP89uXMXn/lWQkdyTosPA== @@ -1428,17 +1388,6 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" -"@emotion/serialize@^0.11.8", "@emotion/serialize@^0.11.9": - version "0.11.9" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.9.tgz#123e0f51d2dee9693fae1057bd7fc27b021d6868" - integrity sha512-/Cn4V81z3ZyFiDQRw8nhGFaHkxHtmCSSBUit4vgTuLA1BqxfJUYiqSq97tq/vV8z9LfIoqs6a9v6QrUFWZpK7A== - dependencies: - "@emotion/hash" "0.7.2" - "@emotion/memoize" "0.7.2" - "@emotion/unitless" "0.7.4" - "@emotion/utils" "0.11.2" - csstype "^2.5.7" - "@emotion/sheet@0.9.3": version "0.9.3" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" @@ -1462,12 +1411,12 @@ "@emotion/styled-base" "^10.0.23" babel-plugin-emotion "^10.0.23" -"@emotion/stylis@0.8.4": +"@emotion/stylis@0.8.4", "@emotion/stylis@^0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c" integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ== -"@emotion/unitless@0.7.4": +"@emotion/unitless@0.7.4", "@emotion/unitless@^0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ== @@ -1477,11 +1426,6 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== -"@emotion/weak-memoize@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27" - integrity sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ== - "@emotion/weak-memoize@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" @@ -3799,13 +3743,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^10.0.1": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.2.tgz#74069f7b1d0cb67b7af99a2584b30e496e545d8b" - integrity sha512-76M5VRbhduUarM9wyMWQm3tLKCVMKTlhG0+W67dteg/HBE+kueIwuyLWzE0m5fmuilvrDXoM5NL890KLnHETZw== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^10.1.0": version "10.1.1" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#7afae39a4247f30c13b8bbb726ccd1b8cda9d4a5" @@ -3813,6 +3750,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@^11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#51d9f37942dd18cc4aa10da98a5c883664e7ee46" + integrity sha512-7ZbT/7mNJu+uRrUGdTQ1hAINtqg909L4NHrXyspV42fvVgBgda6ysiBzoDUMENmQ/RlRJdpyrcp8Dtd/77bp9Q== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -3839,6 +3783,14 @@ resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.17.tgz#e1fc6e46e8af58bdef9531259d509380a8a99e8e" integrity sha512-FGd6J1GQ7zvl1GZ3BBev83B7nfak8dqoR2PZ+l5MoisKMpd4xOLhZJC1ugpmk3Rz5F85t6HbOg9mYqXW97BsNA== +"@types/react-native@*": + version "0.60.22" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.22.tgz#ba199a441cb0612514244ffb1d0fe6f04c878575" + integrity sha512-LTXMKEyGA+x4kadmjujX6yAgpcaZutJ01lC7zLJWCULaZg7Qw5/3iOQpwIJRUcOc+a8A2RR7rSxplehVf9IuhA== + dependencies: + "@types/prop-types" "*" + "@types/react" "*" + "@types/react-redux@^6.0.6": version "6.0.6" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.6.tgz#87f1d0a6ea901b93fcaf95fa57641ff64079d277" @@ -3885,10 +3837,10 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@^16.8.0": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055" - integrity sha512-8gU69ELfJGxzVWVYj4MTtuHxz9nO+d175XeQ1XrXXxesUBsB4KK6OCfzVhEX6leZWWBDVtMJXp/rUjhClzL7gw== +"@types/react-test-renderer@^16.8.3": + version "16.9.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" + integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ== dependencies: "@types/react" "*" @@ -4014,13 +3966,14 @@ dependencies: "@types/node" "*" -"@types/styled-components@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-3.0.2.tgz#274133bfafaca17f28707b667858bce197ae3b84" - integrity sha512-nG9swaAqmSrUDXyjpE0NxabjVYAGlmtqWXlCpRWRIZBMbTkdcyQULC+ElvTfghTc+1ANJjn6DCyUQirF5a2OOg== +"@types/styled-components@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.0.tgz#15a3d59533fd3a5bd013db4a7c4422ec542c59d2" + integrity sha512-QFl+w3hQJNHE64Or3PXMFpC3HAQDiuQLi5o9m1XPEwYWfgCZtAribO5ksjxnO8U0LG8Parh0ESCgVxo4VfxlHg== dependencies: - "@types/node" "*" "@types/react" "*" + "@types/react-native" "*" + csstype "^2.2.0" "@types/superagent@*": version "3.8.4" @@ -4209,24 +4162,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" - integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== +"@typescript-eslint/eslint-plugin@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz#fa810282c0e45f6c2310b9c0dfcd25bff97ab7e9" + integrity sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ== dependencies: - "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" - integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== +"@typescript-eslint/experimental-utils@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz#bbe99a8d9510240c055fc4b17230dd0192ba3c7f" + integrity sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4238,14 +4191,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" - integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== +"@typescript-eslint/parser@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.9.0.tgz#2e9cf438de119b143f642a3a406e1e27eb70b7cd" + integrity sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.8.0" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4256,10 +4209,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" - integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== +"@typescript-eslint/typescript-estree@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz#09138daf8f47d0e494ba7db9e77394e928803017" + integrity sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -4588,7 +4541,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" integrity sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg== -acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.2.1, acorn@^5.5.0: +acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.5.0: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -4723,13 +4676,13 @@ aggregate-error@^3.0.0: string.prototype.padstart "^3.0.0" symbol.prototype.description "^1.0.0" -airbnb-prop-types@^2.13.2: - version "2.13.2" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz#43147a5062dd2a4a5600e748a47b64004cc5f7fc" - integrity sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ== +airbnb-prop-types@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" + integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== dependencies: - array.prototype.find "^2.0.4" - function.prototype.name "^1.1.0" + array.prototype.find "^2.1.0" + function.prototype.name "^1.1.1" has "^1.0.3" is-regex "^1.0.4" object-is "^1.0.1" @@ -4737,7 +4690,7 @@ airbnb-prop-types@^2.13.2: object.entries "^1.1.0" prop-types "^15.7.2" prop-types-exact "^1.2.0" - react-is "^16.8.6" + react-is "^16.9.0" ajv-errors@^1.0.0: version "1.0.0" @@ -4875,15 +4828,10 @@ angular-ui-ace@0.2.3: resolved "https://registry.yarnpkg.com/angular-ui-ace/-/angular-ui-ace-0.2.3.tgz#3cb903428100621a367fc7f641440e97a42a26d0" integrity sha1-PLkDQoEAYho2f8f2QUQOl6QqJtA= -angular@>=1.0.6: - version "1.6.9" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.9.tgz#bc812932e18909038412d594a5990f4bb66c0619" - integrity sha512-6igWH2GIsxV+J38wNWCh8oyjaZsrIPIDO35twloIUyjlF2Yit6UyLAWujHP05ma/LFxTsx4NtYibRoMNBXPR1A== - -angular@^1.7.8: - version "1.7.8" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.8.tgz#b77ede272ce1b261e3be30c1451a0b346905a3c9" - integrity sha512-wtef/y4COxM7ZVhddd7JtAAhyYObq9YXKar9tsW7558BImeVYteJiTxCKeJOL45lJ/+7B4wrAC49j8gTFYEthg== +angular@>=1.0.6, angular@^1.7.9: + version "1.7.9" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.9.tgz#e52616e8701c17724c3c238cfe4f9446fd570bc4" + integrity sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ== ansi-align@^2.0.0: version "2.0.0" @@ -4899,11 +4847,6 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-color@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" - integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o= - ansi-colors@3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" @@ -4992,6 +4935,11 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" @@ -5058,11 +5006,6 @@ any-observable@^0.3.0: resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -5550,7 +5493,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.0.4: +array.prototype.find@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== @@ -6153,6 +6096,16 @@ babel-plugin-react-docgen@^3.0.0: resolved "https://registry.yarnpkg.com/babel-plugin-require-context-hook-babel7/-/babel-plugin-require-context-hook-babel7-1.0.0.tgz#1273d4cee7e343d0860966653759a45d727e815d" integrity sha512-kez0BAN/cQoyO1Yu1nre1bQSYZEF93Fg7VQiBHFfMWuaZTy7vJSTT4FY68FwHTYG53Nyt0A7vpSObSVxwweQeQ== +"babel-plugin-styled-components@>= 1": + version "1.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.6.tgz#f8782953751115faf09a9f92431436912c34006b" + integrity sha512-gyQj/Zf1kQti66100PhrCRjI5ldjaze9O0M3emXRPAN80Zsf8+e1thpTpaXJXVHXtaM4/+dJEgZHyS9Its+8SA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-module-imports" "^7.0.0" + babel-plugin-syntax-jsx "^6.18.0" + lodash "^4.17.11" + babel-plugin-syntax-jsx@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -6421,11 +6374,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base62@^1.1.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" - integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== - base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -6503,6 +6451,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,7 +6732,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace@0.11.1, brace@^0.11.0: +brace@0.11.1, brace@^0.11.0, brace@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= @@ -6834,6 +6787,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -7025,14 +6991,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.3: - version "5.0.8" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.8.tgz#84daa52e7cf2fa8ce4195bc5cf0f7809e0930b24" - integrity sha512-xXvjQhVNz50v2nPeoOsNqWCLGfiv4ji/gXZM28jnVwdLJxH4mFyqgqCKfaK9zf1KUbG6zTkjLOy7ou+jSMarGA== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - buffer@^5.1.0, buffer@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" @@ -7050,15 +7008,6 @@ buffered-spawn@~1.1.1: err-code "^0.1.0" q "^1.0.1" -bufrw@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.2.1.tgz#93f222229b4f5f5e2cd559236891407f9853663b" - integrity sha1-k/IiIptPX14s1VkjaJFAf5hTZjs= - dependencies: - ansi-color "^0.2.1" - error "^7.0.0" - xtend "^4.0.0" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -7340,7 +7289,7 @@ camelcase@^1.0.2: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= -camelcase@^2.0.0: +camelcase@^2.0.0, camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= @@ -7370,6 +7319,11 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelize@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + can-use-dom@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" @@ -7464,13 +7418,6 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chai-as-promised@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" - integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== - dependencies: - check-error "^1.0.2" - chai@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" @@ -7480,7 +7427,7 @@ chai@3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chai@^4.0.1, chai@^4.1.2: +chai@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== @@ -7492,7 +7439,7 @@ chai@^4.0.1, chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" -chalk@2.4.2, chalk@^2.2.0, chalk@^2.3.2, chalk@^2.4.2, chalk@~2.4.1: +chalk@2.4.2, chalk@^2.3.2, chalk@^2.4.2, chalk@~2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -7965,7 +7912,7 @@ cliui@^2.1.0: right-align "^0.1.1" wordwrap "0.0.2" -cliui@^3.2.0: +cliui@^3.0.3, cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= @@ -8061,6 +8008,11 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" +clsx@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec" + integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg== + cmd-shim@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.1.0.tgz#e59a08d4248dda3bb502044083a4db4ac890579a" @@ -8250,7 +8202,7 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2, commander@^2.9.0: +commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -8275,11 +8227,6 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, comm resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.5.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -8309,21 +8256,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commoner@^0.10.1: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - compare-versions@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" @@ -8701,11 +8633,6 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138" - integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA== - core-js@^3.0.1, core-js@^3.0.4, core-js@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" @@ -9032,6 +8959,13 @@ css-box-model@^1.1.1: dependencies: tiny-invariant "^1.0.3" +css-box-model@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0" + integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -9097,14 +9031,14 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-to-react-native@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.0.4.tgz#cf4cc407558b3474d4ba8be1a2cd3b6ce713101b" - integrity sha1-z0zEB1WLNHTUuovhos07bOcTEBs= +css-to-react-native@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756" + integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ== dependencies: + camelize "^1.0.0" css-color-keywords "^1.0.0" - fbjs "^0.8.5" - postcss-value-parser "^3.3.0" + postcss-value-parser "^4.0.2" css-tree@1.0.0-alpha.28: version "1.0.0-alpha.28" @@ -9188,15 +9122,10 @@ cssstyle@^1.1.1: dependencies: cssom "0.3.x" -csstype@^2.2.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" - integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== - -csstype@^2.5.7: - version "2.6.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa" - integrity sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg== +csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" + integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== cuint@^0.2.2: version "0.2.2" @@ -9230,7 +9159,15 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cypress@^3.5.5: +cypress-multi-reporters@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.2.3.tgz#4ba39373631c6521d21931d73f6b0bafa1ccbf83" + integrity sha512-W3ItWsbSgMfsQFTuB89OXY5gyqLuM0O2lNEn+mcQAYeMs36TxVLAg3q+Hk0Om+NcWj8OLhM06lBQpnu9+i4gug== + dependencies: + debug "^4.1.1" + lodash "^4.17.11" + +cypress@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.6.1.tgz#4420957923879f60b7a5146ccbf81841a149b653" integrity sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A== @@ -9553,11 +9490,6 @@ dargs@^5.1.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" integrity sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk= -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -9790,15 +9722,10 @@ deep-object-diff@^1.1.0: resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== -deepmerge@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" - integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== - -deepmerge@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.0.0.tgz#3e3110ca29205f120d7cb064960a39c3d2087c09" - integrity sha512-YZ1rOP5+kHor4hMAH+HRQnBQHg+wvS1un1hAOuIcxcBy0hzcUf6Jg2a1w65kpoOUnurOfZbERwjI1TfZxNjcww== +deepmerge@3.2.0, deepmerge@^4.0.0, deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== default-compare@^1.0.0: version "1.0.0" @@ -9868,7 +9795,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@^1.0.0, defined@~1.0.0: +defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= @@ -10117,14 +10044,6 @@ detective-typescript@^5.1.1: node-source-walk "^4.2.0" typescript "^3.4.5" -detective@^4.3.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" - integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== - dependencies: - acorn "^5.2.1" - defined "^1.0.0" - dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -10159,6 +10078,11 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" +diff-match-patch@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" + integrity sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg== + diff-sequences@^24.0.0: version "24.0.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.0.0.tgz#cdf8e27ed20d8b8d3caccb4e0c0d8fe31a173013" @@ -10286,6 +10210,14 @@ dom-helpers@^3.3.1: dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" + integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== + dependencies: + "@babel/runtime" "^7.6.3" + csstype "^2.6.7" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -10790,46 +10722,47 @@ env-variable@0.0.x: resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA== -envify@^3.0.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" - integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg= +enzyme-adapter-react-16@^1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" + integrity sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA== dependencies: - jstransform "^11.0.3" - through "~2.3.4" - -enzyme-adapter-react-16@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.14.0.tgz#204722b769172bcf096cb250d33e6795c1f1858f" - integrity sha512-7PcOF7pb4hJUvjY7oAuPGpq3BmlCig3kxXGi2kFx0YzJHppqX1K8IIV9skT1IirxXlu8W7bneKi+oQ10QRnhcA== - dependencies: - enzyme-adapter-utils "^1.12.0" + enzyme-adapter-utils "^1.12.1" + enzyme-shallow-equal "^1.0.0" has "^1.0.3" object.assign "^4.1.0" object.values "^1.1.0" prop-types "^15.7.2" - react-is "^16.8.6" + react-is "^16.10.2" react-test-renderer "^16.0.0-0" semver "^5.7.0" -enzyme-adapter-utils@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93" - integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA== +enzyme-adapter-utils@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz#e828e0d038e2b1efa4b9619ce896226f85c9dd88" + integrity sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g== dependencies: - airbnb-prop-types "^2.13.2" - function.prototype.name "^1.1.0" + airbnb-prop-types "^2.15.0" + function.prototype.name "^1.1.1" object.assign "^4.1.0" - object.fromentries "^2.0.0" + object.fromentries "^2.0.1" prop-types "^15.7.2" - semver "^5.6.0" + semver "^5.7.0" -enzyme-to-json@^3.3.4: - version "3.3.5" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.5.tgz#f8eb82bd3d5941c9d8bc6fd9140030777d17d0af" - integrity sha512-DmH1wJ68HyPqKSYXdQqB33ZotwfUhwQZW3IGXaNXgR69Iodaoj8TF/D9RjLdz4pEhGq2Tx2zwNUIjBuqoZeTgA== +enzyme-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69" + integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ== dependencies: - lodash "^4.17.4" + has "^1.0.3" + object-is "^1.0.1" + +enzyme-to-json@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.3.tgz#ed4386f48768ed29e2d1a2910893542c34e7e0af" + integrity sha512-jqNEZlHqLdz7OTpXSzzghArSS3vigj67IU/fWkPyl1c0TCj9P5s6Ze0kRkYZWNEoCqCR79xlQbigYlMx5erh8A== + dependencies: + lodash "^4.17.15" enzyme@^3.10.0: version "3.10.0" @@ -10884,7 +10817,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error@7.0.2, error@^7.0.0, error@^7.0.2: +error@^7.0.0, error@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" integrity sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI= @@ -10892,41 +10825,23 @@ error@7.0.2, error@^7.0.0, error@^7.0.2: string-template "~0.2.1" xtend "~4.0.0" -es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== +es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.14.2, es-abstract@^1.15.0, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== dependencies: es-to-primitive "^1.2.0" function-bind "^1.1.1" has "^1.0.3" + has-symbols "^1.0.0" is-callable "^1.1.4" is-regex "^1.0.4" - object-keys "^1.0.12" - -es-abstract@^1.4.3, es-abstract@^1.9.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" - integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-abstract@^1.5.1, es-abstract@^1.7.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" - integrity sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ== - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" -es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: +es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== @@ -11498,11 +11413,6 @@ espree@^6.1.1: acorn-jsx "^5.0.2" eslint-visitor-keys "^1.1.0" -esprima-fb@^15001.1.0-dev-harmony-fb: - version "15001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" - integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= - esprima@2.7.x, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -11723,21 +11633,6 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" - integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== - dependencies: - cross-spawn "^6.0.5" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" @@ -12144,13 +12039,6 @@ fast-glob@^3.0.3: merge2 "^1.2.3" micromatch "^4.0.2" -fast-json-patch@^2.0.2: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-2.0.7.tgz#55864b08b1e50381d2f37fd472bb2e18fe54a733" - integrity sha512-DQeoEyPYxdTtfmB3yDlxkLyKTdbJ6ABfFGcMynDqjvGhPYLto/pZyb/dG2Nyd/n9CArjEWN9ZST++AFmgzgbGw== - dependencies: - deep-equal "^1.0.1" - fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -12199,17 +12087,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" - integrity sha1-lja3cF9bqWhNRLcveDISVK/IYPc= - dependencies: - core-js "^1.0.0" - loose-envify "^1.0.0" - promise "^7.0.3" - ua-parser-js "^0.7.9" - whatwg-fetch "^0.9.0" - fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -12223,7 +12100,7 @@ fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fbjs@^0.8.4, fbjs@^0.8.5, fbjs@^0.8.9: +fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" integrity sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s= @@ -13076,11 +12953,26 @@ function.prototype.name@^1.1.0: function-bind "^1.1.1" is-callable "^1.1.3" +function.prototype.name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392" + integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + functions-have-names "^1.1.1" + is-callable "^1.1.4" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" + integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== + fuse.js@^3.4.4: version "3.4.5" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6" @@ -14289,18 +14181,6 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" -gulp-mocha@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408" - integrity sha512-ZXBGN60TXYnFhttr19mfZBOtlHYGx9SvCSc+Kr/m2cMIGloUe176HBPwvPqlakPuQgeTGVRS47NmcdZUereKMQ== - dependencies: - dargs "^7.0.0" - execa "^2.0.4" - mocha "^6.2.0" - plugin-error "^1.0.1" - supports-color "^7.0.0" - through2 "^3.0.1" - gulp-rename@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd" @@ -14374,10 +14254,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.3.5.tgz#d6c2d0a0f08b4479e3949f8321c0f3893bb691be" - integrity sha512-I16T/l8X9DV3sEkY9sK9lsPRgDsj82ayBY/4pAZyP2BcX5WeRM3O06bw9kIs2GLrHvFB/DNzWWJyFvof8wQGqw== +handlebars@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -14585,14 +14465,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg= - dependencies: - function-bind "^1.0.2" - -has@^1.0.1, has@^1.0.3, has@~1.0.3: +has@^1.0.0, has@^1.0.1, has@^1.0.3, has@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -15084,7 +14957,7 @@ icalendar@0.7.1: resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" integrity sha1-0NNIZ5X48cXPT4yvrAgbS056Mq4= -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -16566,22 +16439,6 @@ iterall@^1.1.3, iterall@^1.2.1: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== -iterare@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.1.2.tgz#32e65fe03c72f727b1ae5fd002ed6a215f523ae8" - integrity sha512-25rVYmj/dDvTR6zOa9jY1Ihd6USLa0J508Ub2iy7Aga+xu9JMbjDds2Uh03ReDGbva/YN3s3Ybi+Do0nOX6wAg== - -jaeger-client@^3.5.3: - version "3.13.0" - resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.13.0.tgz#c5b228242d65389a13eb24eeb56a55409d72c94e" - integrity sha512-ykrXLxcmSHSdDXqK6/DY+IObekfj4kbONC3QPu/ln7sbY5bsA+Yu4LYVlW9/vLm0lxLlsz52mSyC+sjiqM8xCw== - dependencies: - node-int64 "^0.4.0" - opentracing "^0.13.0" - thriftrw "^3.5.0" - uuid "^3.2.1" - xorshift "^0.2.0" - jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -16933,10 +16790,10 @@ jest-specific-snapshot@^2.0.0: dependencies: jest-snapshot "^24.1.0" -jest-styled-components@^6.3.3: - version "6.3.3" - resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-6.3.3.tgz#e15bbda13a6b6ff876d6b783751fe9840860c52a" - integrity sha512-RBMPZSJJSgPDTTJsuYzx5fsij/CULaqQNZOWkn8J/L++rX6P830o2vB9CXGzfQf/bVq9qGr1ZBNoivi+v6JPYg== +jest-styled-components@^7.0.0-beta.2: + version "7.0.0-beta.2" + resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-7.0.0-beta.2.tgz#0442a8491a2411ea3fad0b1594214112722c173d" + integrity sha512-0t3FjoCoQPhmHxPZXm/zI9jAVnWGQERVr7hpRjN1EKyEH8R4YE8bPwA0BmA/9UTcL9iznODr1VDmz+9yXylpNw== dependencies: css "^2.2.4" @@ -17057,6 +16914,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -17342,17 +17204,6 @@ jssha@^2.1.0: resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a" integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po= -jstransform@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" - integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= - dependencies: - base62 "^1.1.0" - commoner "^0.10.1" - esprima-fb "^15001.1.0-dev-harmony-fb" - object-assign "^2.0.0" - source-map "^0.4.2" - jstransformer-ejs@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/jstransformer-ejs/-/jstransformer-ejs-0.0.3.tgz#04d9201469274fcf260f1e7efd732d487fa234b6" @@ -17835,14 +17686,6 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" -line-column@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" - integrity sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI= - dependencies: - isarray "^1.0.0" - isobject "^2.0.0" - linebreak@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/linebreak/-/linebreak-0.3.0.tgz#0526480a62c05bd679f3e9d99830e09c6a7d0ed6" @@ -18432,7 +18275,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -18526,11 +18369,6 @@ lolex@^4.1.0, lolex@^4.2.0: resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== -long@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" - integrity sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8= - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -18785,6 +18623,13 @@ mapbox-gl@1.3.1: tinyqueue "^2.0.0" vt-pbf "^3.1.1" +marge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/marge/-/marge-1.0.1.tgz#52d6026911e62e1dd1cf60a07313dde285a8370c" + integrity sha1-UtYCaRHmLh3Rz2CgcxPd4oWoNww= + dependencies: + yargs "^3.15.0" + markdown-escapes@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.1.tgz#1994df2d3af4811de59a6714934c2b2292734518" @@ -18946,6 +18791,11 @@ memoize-one@^5.0.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.2.tgz#6aba5276856d72fb44ead3efab86432f94ba203d" integrity sha512-o7lldN4fs/axqctc03NF+PMhd2veRrWeJ2n2GjEzUPBD4F9rmNg4A+bQCACIzwjHJEXuYv4aFFMaH35KZfHUrw== +memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -19122,6 +18972,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19445,18 +19300,10 @@ mocha-junit-reporter@^1.23.1: strip-ansi "^4.0.0" xml "^1.0.0" -mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= - dependencies: - debug "^3.1.0" - lodash "^4.16.4" - -mocha@6.2.1, mocha@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" - integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== +mocha@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" + integrity sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A== dependencies: ansi-colors "3.2.3" browser-stdout "1.3.1" @@ -19705,20 +19552,18 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -mz@^2.6.0, mz@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - nan@^2.10.0, nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -20274,13 +20119,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - npm-run-path@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" @@ -20438,15 +20276,15 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-hash@^1.1.8, object-hash@^1.3.1: +object-hash@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== -object-inspect@^1.6.0, object-inspect@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" - integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== +object-inspect@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== object-inspect@~0.4.0: version "0.4.0" @@ -20458,21 +20296,21 @@ object-inspect@~1.4.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4" integrity sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw== +object-inspect@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.9: +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.0.9, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-keys@^1.0.6: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - integrity sha1-xUYBd4rVYPEULODgG8yotW0TQm0= - object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" @@ -20537,15 +20375,15 @@ object.fromentries@^1.0.0: function-bind "^1.1.1" has "^1.0.1" -object.fromentries@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" - integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== +object.fromentries@^2.0.0, object.fromentries@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" + integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== dependencies: - define-properties "^1.1.2" - es-abstract "^1.11.0" + define-properties "^1.1.3" + es-abstract "^1.15.0" function-bind "^1.1.1" - has "^1.0.1" + has "^1.0.3" object.getownpropertydescriptors@^2.0.3: version "2.0.3" @@ -20648,7 +20486,7 @@ one-time@0.0.4: onetime@^1.0.0: version "1.1.0" - resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= onetime@^2.0.0: @@ -20677,16 +20515,6 @@ opener@^1.4.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== -opentracing@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.13.0.tgz#6a341442f09d7d866bc11ed03de1e3828e3d6aab" - integrity sha1-ajQUQvCdfYZrwR7QPeHjgo49aqs= - -opentracing@^0.14.0: - version "0.14.3" - resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.3.tgz#23e3ad029fa66a653926adbe57e834469f8550aa" - integrity sha1-I+OtAp+mamU5Jq2+V+g0Rp+FUKo= - opn@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" @@ -21192,11 +21020,6 @@ parse-git-config@^1.1.1: git-config-path "^1.0.1" ini "^1.3.4" -parse-gitignore@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-gitignore/-/parse-gitignore-1.0.1.tgz#8b9dc57f17b810d495c5dfa62eb07caffe7758c7" - integrity sha512-UGyowyjtx26n65kdAMWhm6/3uy5uSrpcuH7tt+QEVudiBoVS+eqHxD5kbi9oWVRwj7sCzXqwuM+rUGw7earl6A== - parse-headers@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536" @@ -21889,6 +21712,11 @@ postcss-value-parser@^4.0.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== +postcss-value-parser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" + integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== + postcss-values-parser@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz#5d9fa63e2bcb0179ce48f3235303765eb89f3047" @@ -22078,7 +21906,7 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.9.0" function-bind "^1.1.1" -promise@^7.0.1, promise@^7.0.3, promise@^7.1.1: +promise@^7.0.1, promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== @@ -22120,7 +21948,7 @@ prop-types@15.6.1: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -22529,6 +22357,11 @@ raf-schd@^4.0.0: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.0.tgz#9855756c5045ff4ed4516e14a47719387c3c907b" integrity sha512-m7zq0JkIrECzw9mO5Zcq6jN4KayE34yoIS9hJoiZNXyOAT06PPA8PrR+WtJIeFW09YjUfNkMMN9lrmAt6BURCA== +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + raf@^3.1.0, raf@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -22685,6 +22518,17 @@ react-ace@^5.9.0: lodash.isequal "^4.1.1" prop-types "^15.5.8" +react-ace@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01" + integrity sha512-3iI+Rg2bZXCn9K984ll2OF4u9SGcJH96Q1KsUgs9v4M2WePS4YeEHfW2nrxuqJrAkE5kZbxaCE79k6kqK0YBjg== + dependencies: + brace "^0.11.1" + diff-match-patch "^1.0.4" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + react-addons-create-fragment@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-15.6.2.tgz#a394de7c2c7becd6b5475ba1b97ac472ce7c74f8" @@ -22713,7 +22557,7 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: +react-beautiful-dnd@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#d753088d77d7632e77cf8a8935fafcffa38f574b" integrity sha512-TdE06Shfp56wm28EzjgC56EEMgGI5PDHejJ2bxuAZvZr8CVsbksklsJC06Hxf0MSL7FHbflL/RpkJck9isuxHg== @@ -22727,6 +22571,19 @@ react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: redux "^4.0.1" tiny-invariant "^1.0.4" +react-beautiful-dnd@^12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#810f9b9d94f667b15b253793e853d016a0f3f07c" + integrity sha512-w/mpIXMEXowc53PCEnMoFyAEYFgxMfygMK5msLo5ifJ2/CiSACLov9A79EomnPF7zno3N207QGXsraBxAJnyrw== + dependencies: + "@babel/runtime-corejs2" "^7.6.3" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-beautiful-dnd@^8.0.7: version "8.0.7" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" @@ -22851,17 +22708,7 @@ react-docgen@^4.1.0: node-dir "^0.1.10" recast "^0.17.3" -react-dom@^16.2.0, react-dom@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3" - integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react-dom@^16.8.3, react-dom@^16.8.5: +react-dom@16.8.6, react-dom@^16.8.3, react-dom@^16.8.5, react-dom@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -23019,12 +22866,12 @@ react-intl@^2.8.0: intl-relativeformat "^2.1.0" invariant "^2.1.1" -react-is@^16.3.1: - version "16.4.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" - integrity sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ== +react-is@^16.10.2, react-is@^16.9.0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" + integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.6: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -23201,6 +23048,18 @@ react-redux@^5.1.1: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -23349,7 +23208,7 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-test-renderer@^16.0.0-0: +react-test-renderer@16.8.6, react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== @@ -23359,16 +23218,6 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.6" scheduler "^0.13.6" -react-test-renderer@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.2.tgz#3ce0bf12aa211116612fda01a886d6163c9c459b" - integrity sha512-gsd4NoOaYrZD2R8zi+CBV9wTGMsGhE2bRe4wvenGy0WcLJgdPscRZDDz+kmLjY+/5XpYC8yRR/v4CScgYfGyoQ== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.2" - scheduler "^0.13.2" - react-testing-library@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d" @@ -23415,6 +23264,18 @@ react-virtualized@^9.18.5: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" +react-virtualized@^9.21.2: + version "9.21.2" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" + integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== + dependencies: + babel-runtime "^6.26.0" + clsx "^1.0.1" + dom-helpers "^5.0.0" + loose-envify "^1.3.0" + prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + react-vis@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.8.2.tgz#0e0aebc427e50856a01b666569ffad0411ef050f" @@ -23444,25 +23305,7 @@ react-visibility-sensor@^5.1.1: dependencies: prop-types "^15.7.2" -react@^0.14.0: - version "0.14.9" - resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" - integrity sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE= - dependencies: - envify "^3.0.0" - fbjs "^0.6.1" - -react@^16.2.0, react@^16.6.0, react@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c" - integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react@^16.8.3, react@^16.8.5: +react@16.8.6, react@^0.14.0, react@^16.8.3, react@^16.8.5, react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -23695,16 +23538,6 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" -recast@^0.11.17, recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -23725,6 +23558,16 @@ recast@^0.17.3: private "^0.1.8" source-map "~0.6.1" +recast@~0.11.12: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -23850,6 +23693,14 @@ redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -24589,6 +24440,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24596,13 +24454,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -24744,7 +24595,7 @@ rx@^4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= -rxjs@^5.0.0-beta.11, rxjs@^5.5.0, rxjs@^5.5.2: +rxjs@^5.0.0-beta.11, rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== @@ -24768,6 +24619,11 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + safe-json-parse@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" @@ -24905,7 +24761,7 @@ saxes@^3.1.3: dependencies: xmlchars "^2.1.1" -scheduler@^0.13.2, scheduler@^0.13.3, scheduler@^0.13.6: +scheduler@^0.13.3, scheduler@^0.13.6: version "0.13.6" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== @@ -25019,11 +24875,6 @@ selfsigned@^1.10.7: dependencies: node-forge "0.9.0" -semaphore-async-await@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz#857bef5e3644601ca4b9570b87e9df5ca12974fa" - integrity sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo= - semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -25649,34 +25500,10 @@ source-map-support@^0.3.2: dependencies: source-map "0.1.32" -source-map-support@^0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" - integrity sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.6: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.9: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== +source-map-support@^0.5.1, source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -25693,10 +25520,10 @@ source-map@0.1.32: dependencies: amdefine ">=0.0.4" -"source-map@>= 0.1.2", source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +"source-map@>= 0.1.2": + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== source-map@^0.4.2: version "0.4.4" @@ -25710,6 +25537,11 @@ source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, sour resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + source-map@~0.1.30, source-map@~0.1.33: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -26132,11 +25964,6 @@ string-replace-loader@^2.2.0: loader-utils "^1.2.3" schema-utils "^1.0.0" -string-similarity@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-2.0.0.tgz#c16d8fc7e7c8dce706742c87adc482dbdb7030bb" - integrity sha512-62FBZrVXV5cI23bQ9L49Y4d9u9yaH61JhAwLyUFUzQbHDjdihxdfCwIherg+vylR/s4ucCddK8iKSEO7kinffQ== - string-template@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" @@ -26159,16 +25986,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1" - integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.0.0" - -string-width@^3.1.0: +string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -26178,21 +25996,21 @@ string-width@^3.1.0: strip-ansi "^5.1.0" string-width@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" - integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^5.2.0" + strip-ansi "^6.0.0" string.prototype.matchall@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.0.tgz#66f4d8dd5c6c6cea4dffb55ec5f3184a8dd0dd59" - integrity sha512-/g0YW/cEfXASRHAaLR7VZbTUlxgP14fmCsfSRFG2gvlG2S1q9rBpjYnEy/EIIzY+bjzs2nTfAHJYXmQ+zTnXSQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.2.tgz#c1fdb23f90058e929a69cfa2e8b12300daefe030" + integrity sha512-hsRe42jQ8+OJej2GVjhnSVodQ3NQgHV0FDD6dW7ZTM22J4uIbuYiAADCCc1tfyN7ocEl/KUUbudM36E2tZcF8w== dependencies: - define-properties "^1.1.2" - es-abstract "^1.12.0" + define-properties "^1.1.3" + es-abstract "^1.14.2" function-bind "^1.1.1" has-symbols "^1.0.0" regexp.prototype.flags "^1.2.0" @@ -26215,7 +26033,16 @@ string.prototype.padstart@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" -string.prototype.trim@^1.1.2, string.prototype.trim@~1.1.2: +string.prototype.trim@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d" + integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + function-bind "^1.1.1" + +string.prototype.trim@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" integrity sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo= @@ -26224,24 +26051,33 @@ string.prototype.trim@^1.1.2, string.prototype.trim@~1.1.2: es-abstract "^1.5.0" function-bind "^1.0.2" +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -string_decoder@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.0.tgz#384f322ee8a848e500effde99901bba849c5d403" - integrity sha512-8zQpRF6juocE69ae7CSPmYEGJe4VCXwP6S6dxUWI7i53Gwv54/ec41fiUA+X7BPGGv7fRSQJjBQVa0gomGaOgg== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" - integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - safe-buffer "~5.1.0" + safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" @@ -26273,7 +26109,14 @@ stringstream@~0.0.4, stringstream@~0.0.5: resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== -strip-ansi@*, strip-ansi@5.2.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@*, strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -26301,13 +26144,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== - dependencies: - ansi-regex "^4.0.0" - strip-ansi@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" @@ -26413,31 +26249,27 @@ style-loader@0.23.1, style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" -styled-components@3.4.10: - version "3.4.10" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" - integrity sha512-TA8ip8LoILgmSAFd3r326pKtXytUUGu5YWuqZcOQVwVVwB6XqUMn4MHW2IuYJ/HAD81jLrdQed8YWfLSG1LX4Q== +styled-components@beta: + version "5.0.0-rc.2" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.0.0-rc.2.tgz#6c44570ff202f47a1a688d6f1249bc079b10a958" + integrity sha512-dRMU2Ka12F2qbJK6XMDVy1H6KOXpbf7nAKReV0uIikCdW/zbO2K3C+XUCL0EqTVeevugFBJUACZUoTc7ShKsTg== dependencies: - buffer "^5.0.3" - css-to-react-native "^2.0.3" - fbjs "^0.8.16" - hoist-non-react-statics "^2.5.0" - prop-types "^15.5.4" - react-is "^16.3.1" - stylis "^3.5.0" + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^0.8.3" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1" + css-to-react-native "^3.0.0" + shallowequal "^1.1.0" stylis-rule-sheet "^0.0.10" - supports-color "^3.2.3" + supports-color "^5.5.0" stylis-rule-sheet@^0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== -stylis@^3.5.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.1.tgz#fd341d59f57f9aeb412bc14c9d8a8670b438e03b" - integrity sha512-yM4PyeHuwhIOUHNJxi1/Mbq8kVLv4AkyE7IYLP/LK0lIFcr3tRa2H1iZlBYKIxOlf+/jruBTe8DdKSyQX9w4OA== - stylus-lookup@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd" @@ -26546,7 +26378,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.0, supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -26964,20 +26796,6 @@ textextensions@2: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286" integrity sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA== -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.0" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" - integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk= - dependencies: - any-promise "^1.0.0" - thread-loader@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/thread-loader/-/thread-loader-2.1.3.tgz#cbd2c139fc2b2de6e9d28f62286ab770c1acbdda" @@ -26987,15 +26805,6 @@ thread-loader@^2.1.3: loader-utils "^1.1.0" neo-async "^2.6.0" -thriftrw@^3.5.0: - version "3.11.3" - resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.11.3.tgz#2cef6b4d089b7ba6275198b86582881582907d45" - integrity sha512-mnte80Go5MCfYyOQ9nk6SljaEicCXlwLchupHR+/zlx0MKzXwAiyt38CHjLZVvKtoyEzirasXuNYtkEjgghqCw== - dependencies: - bufrw "^1.2.1" - error "7.0.2" - long "^2.4.0" - throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" @@ -27130,6 +26939,11 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== +tiny-invariant@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + tiny-lr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" @@ -28066,7 +27880,7 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@3.5.3, typescript@3.7.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3: +typescript@3.5.3, typescript@3.7.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.5.3: version "3.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== @@ -28363,6 +28177,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -28542,6 +28364,11 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" @@ -28675,7 +28502,7 @@ utils-regex-from-string@^1.0.0: regex-regex "^1.0.0" validate.io-string-primitive "^1.0.0" -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: +uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -29306,12 +29133,12 @@ vscode-languageserver-protocol@3.14.1: vscode-jsonrpc "^4.0.0" vscode-languageserver-types "3.14.0" -vscode-languageserver-types@3.14.0, vscode-languageserver-types@^3.0.3, vscode-languageserver-types@^3.14.0: +vscode-languageserver-types@3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== -vscode-languageserver@^5.0.0, vscode-languageserver@^5.2.1: +vscode-languageserver@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz#0d2feddd33f92aadf5da32450df498d52f6f14eb" integrity sha512-GuayqdKZqAwwaCUjDvMTAVRPJOp/SLON3mJ07eGsx/Iq9HjRymhKWztX41rISqDKhHVVyFM+IywICyZDla6U3A== @@ -29705,11 +29532,6 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== -whatwg-fetch@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" - integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA= - whatwg-mimetype@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" @@ -29818,6 +29640,11 @@ window-size@0.1.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= +window-size@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY= + window-size@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" @@ -30177,11 +30004,6 @@ xmlhttprequest-ssl@~1.5.4: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= -xorshift@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-0.2.1.tgz#fcd82267e9351c13f0fb9c73307f25331d29c63a" - integrity sha1-/NgiZ+k1HBPw+5xzMH8lMx0pxjo= - xpath@0.0.27: version "0.0.27" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" @@ -30218,7 +30040,7 @@ xxhashjs@^0.2.1: dependencies: cuint "^0.2.2" -y18n@^3.2.1: +y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= @@ -30416,6 +30238,19 @@ yargs@^14.2.0: y18n "^4.0.0" yargs-parser "^15.0.0" +yargs@^3.15.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" + integrity sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU= + dependencies: + camelcase "^2.0.1" + cliui "^3.0.3" + decamelize "^1.1.1" + os-locale "^1.4.0" + string-width "^1.0.1" + window-size "^0.1.4" + y18n "^3.2.0" + yargs@^7.0.0, yargs@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" @@ -30461,11 +30296,6 @@ yarn-install@^0.5.1: chalk "^1.1.3" cross-spawn "^4.0.2" -yarn@^1.12.3: - version "1.17.3" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.17.3.tgz#60e0b77d079eb78e753bb616f7592b51b6a9adce" - integrity sha512-CgA8o7nRZaQvmeF/WBx2FC7f9W/0X59T2IaLYqgMo6637wfp5mMEsM3YXoJtKUspnpmDJKl/gGFhnqS+sON7hA== - yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"