diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de7159489689e..7901bd331edff 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -69,9 +69,11 @@ # Canvas /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -# Logs & Metrics UI +# Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui +/x-pack/plugins/infra/ @elastic/logs-metrics-ui /x-pack/legacy/plugins/integrations_manager/ @elastic/epm +/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui @@ -146,6 +148,7 @@ # Kibana Alerting Services /x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services /x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services +/x-pack/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/event_log/ @elastic/kibana-alerting-services /x-pack/plugins/task_manager/ @elastic/kibana-alerting-services /x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index d95205026f5e9..99ba827b0198d 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -45,20 +45,18 @@ Service environments are defined when configuring your APM agents. It's very important to be consistent when naming environments in your agents. See the documentation for each agent you're using to learn how to configure service environments: -|=== -|*Environment configuration* -v|*Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] -*Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] -*Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] -*Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] -*Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] -*Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] -|=== +* *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] +* *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] +* *.NET* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] +* *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] +* *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] +* *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] +* *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] [[contextual-filters]] ==== Contextual filters -Local filters are ways you can filter your specific APM data on each individual page. +Contextual filters are ways you can filter your specific APM data on each individual page. The filters shown are relevant to your data, and will persist between pages, but only where they are applicable -- they are typically most useful in their original context. As an example, if you select a host on the Services overview, then select a transaction group, diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index 2e870c9a030a9..d998b5daedd9b 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/apm/images/apm-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png index 127ac1559e2c3..e9c6713361c73 100644 Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ diff --git a/docs/apm/images/apm-error-group.png b/docs/apm/images/apm-error-group.png index 621df834b8bc0..ecdf9c20cf4aa 100644 Binary files a/docs/apm/images/apm-error-group.png and b/docs/apm/images/apm-error-group.png differ diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index fcc5819623e10..905487d2802bc 100644 Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ diff --git a/docs/apm/images/apm-errors-watcher-assistant.png b/docs/apm/images/apm-errors-watcher-assistant.png index bc43a71e0abdc..1a4d6b5b4c0ea 100644 Binary files a/docs/apm/images/apm-errors-watcher-assistant.png and b/docs/apm/images/apm-errors-watcher-assistant.png differ diff --git a/docs/apm/images/apm-metrics.png b/docs/apm/images/apm-metrics.png index 6a9789b5a6ecd..60383ef428f2a 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/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index 884b716fc43d0..bacb2d372c166 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-transaction-annotation.png b/docs/apm/images/apm-transaction-annotation.png new file mode 100644 index 0000000000000..bc71b1d2169c4 Binary files /dev/null and b/docs/apm/images/apm-transaction-annotation.png differ diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index 6f902a6428bf3..2309ec2435c81 100644 Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index b707ad7c76f09..73668b094f9cf 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 160255cd7bc03..c3c10fcb35ea8 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/apm-transactions-table.png b/docs/apm/images/apm-transactions-table.png new file mode 100644 index 0000000000000..b573adfb0c450 Binary files /dev/null and b/docs/apm/images/apm-transactions-table.png differ diff --git a/docs/apm/images/jvm-metrics.png b/docs/apm/images/jvm-metrics.png index ffeab27e10246..0ca2147ae0e43 100644 Binary files a/docs/apm/images/jvm-metrics.png and b/docs/apm/images/jvm-metrics.png differ diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index 75eae61b4cf12..b1d54ce49c7cd 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 app in Kibana] +image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the 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. @@ -35,6 +35,3 @@ These transactions can be expanded and viewed in detail by clicking on them. 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 app in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 33f61adc8be63..9c21a569f152c 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -2,37 +2,46 @@ === Transaction overview TIP: A {apm-overview-ref-v}/transactions.html[transaction] describes an event captured by an Elastic APM agent instrumenting a service. -The APM agents automatically collect performance metrics on HTTP requests, database queries, and much more. +APM agents automatically collect performance metrics on HTTP requests, database queries, and much more. Selecting a <> brings you to the *transactions* overview. -The *time spent by span type*, *transaction duration* and *requests per minute* chart display information on all transactions associated with the selected service. -The *Transactions* table, however, provides only a list of _transaction groups_ for the selected service. -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 app in Kibana] -*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? +The *time spent by span type*, *transaction duration*, and *requests per minute* chart display information on all transactions associated with the selected service: +*Time spent by span type*:: +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? ++ The time a transaction took to complete is also recorded and displayed on the chart under the "app" label. "app" indicates that something was happening within the application, but we're not sure exactly what. This could be a sign that the agent does not have auto-instrumentation for whatever was happening during that time. - ++ It's important to note that if you have asynchronous spans, the sum of all span times may exceed the duration of the transaction. -*Transaction duration* shows the response times for this service and is broken down into average, 95th, and 99th percentile. +*Transaction duration*:: +Response times for this service, 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, and all of the data on the page will update accordingly. -*Requests per minute* is divided into response codes: 2xx, 3xx, 4xx, etc., +*Requests per minute*:: +Visualize response codes: `2xx`, `3xx`, `4xx`, etc., and is useful for determining if you're serving more of one code than you typically do. Like in the Transaction duration graph, you can zoom in on anomalies to further investigate them. -The *Transactions* table is similar to the <> overview and shows the name of each transaction occurring in the selected service. -Transactions with the same name are grouped together and only shown once in this table. +[[transactions-table]] +==== Transactions table + +The *Transactions* table displays a list of _transaction groups_ for the selected service. +In other words, this view groups all transactions of the same name together, +and only displays one entry for each group. + +[role="screenshot"] +image::apm/images/apm-transactions-table.png[Example view of the transactions table in the APM app in Kibana] + By default, transaction groups are sorted by _Impact_. Impact helps show the most used and slowest endpoints in your service - in other words, it's the collective amount of pain a specific endpoint is causing your users. @@ -40,16 +49,27 @@ If there's a particular endpoint you're worried about, you can click on it to vi [IMPORTANT] ==== -The transaction overview will only display helpful information when the transactions in your service are named correctly. - -Elastic APM Agents come with built-in support for popular frameworks out-of-the-box. -However, if you only see one route in the Transaction overview page, or if you have transactions named "unknown route", +If you only see one route in the Transactions table, or if you have transactions named "unknown route", it could be a symptom that the agent either wasn't installed correctly or doesn't support your framework. For further details, including troubleshooting and custom implementation instructions, refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. ==== +[[transactions-annotations]] +==== Transaction annotations + +For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. +This feature automatically tags new deployments, so you can easily see if your deploy has increased response times +for an end-user, or if the memory/CPU footprint of your application has increased. +Being able to quickly identify bad deployments enables you to rollback and fix issues without causing costly outages. + +Deployment annotations are automatically enabled, and appear when the `service.version` of your app changes. + +[role="screenshot"] +image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] + + [[rum-transaction-overview]] ==== RUM Transaction overview @@ -75,9 +95,9 @@ It's important to note that all of these graphs show data from every transaction [role="screenshot"] image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] -A single sampled transaction is also displayed. -This sampled transaction is based on your selection in the *Transactions duration distribution*. -You can update the sampled transaction by selecting a new _bucket_ in the transactions duration distribution graph. +Up to ten sampled transactions are also displayed. +These sampled transactions are based on your selection in the *Transactions duration distribution*. +You can update the sampled transactions by selecting a new _bucket_ in the transactions duration distribution graph. The number of requests per bucket is displayed when hovering over the graph, and the selected bucket is highlighted to stand out. [role="screenshot"] @@ -85,13 +105,14 @@ image::apm/images/apm-transaction-duration-dist.png[Example view of transactions Let's look at an example. In the screenshot below, -you'll notice most of our requests fall into buckets on the left side of the graph, +you'll notice most of the requests fall into buckets on the left side of the graph, with a long tail of smaller buckets to the right. This is a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. -By clicking on these buckets, -we're presented with a span timeline waterfall showing what a typical request in that bucket was doing. -By investigating this timeline waterfall, we can hopefully see why it was slow and then implement a fix. +When you select one of these buckets, +you're presented with up to ten trace samples. +Each sample has a span timeline waterfall that shows what a typical request in that bucket was doing. +By investigating this timeline waterfall, we can hopefully determine _why_ this request was slow and then implement a fix. [role="screenshot"] image::apm/images/apm-transaction-sample.png[Example view of transactions sample] diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 27ef70d871856..4faa1bc8e542f 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "license": "Apache-2.0", "scripts": { - "interpreter:peg": "pegjs common/lib/grammar.peg", + "interpreter:peg": "pegjs src/common/lib/grammar.peg", "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --dev --watch" diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.js index 9882f3abde723..3b22704b9e9c8 100644 --- a/packages/kbn-interpreter/src/common/lib/registry.js +++ b/packages/kbn-interpreter/src/common/lib/registry.js @@ -31,9 +31,7 @@ export class Registry { } register(fn) { - if (typeof fn !== 'function') throw new Error(`Register requires an function`); - - const obj = fn(); + const obj = typeof fn === 'function' ? fn() : fn; if (typeof obj !== 'object' || !obj[this._prop]) { throw new Error(`Registered functions must return an object with a ${this._prop} property`); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index f3e401bedcef3..eec75033e8beb 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -88442,7 +88442,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -88653,7 +88653,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -88844,7 +88844,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -101921,7 +101921,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -104780,7 +104780,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { diff --git a/packages/kbn-utility-types/README.md b/packages/kbn-utility-types/README.md index 9707ff5a1ed9c..aafae4d3a5134 100644 --- a/packages/kbn-utility-types/README.md +++ b/packages/kbn-utility-types/README.md @@ -24,3 +24,4 @@ type B = UnwrapPromise; // string - `ShallowPromise` — Same as `Promise` type, but it flat maps the wrapped type. - `UnwrapObservable` — Returns wrapped type of an observable. - `UnwrapPromise` — Returns wrapped type of a promise. +- `UnwrapPromiseOrReturn` — Returns wrapped type of a promise or the type itself, if it isn't a promise. diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 83a41a52aca38..ec81f7347b481 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -35,6 +35,11 @@ export type ShallowPromise = T extends Promise ? Promise : Promis */ export type UnwrapPromise> = PromiseType; +/** + * Returns wrapped type of a promise, or returns type as is, if it is not a promise. + */ +export type UnwrapPromiseOrReturn = T extends Promise ? U : T; + /** * Minimal interface for an object resembling an `Observable`. */ diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 27dde2f10703e..e1f7eeff93471 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -40,6 +40,7 @@ const createSetupContractMock = () => { setupContract.getUpdate$.mockReturnValue(new Rx.Subject()); setupContract.getSaved$.mockReturnValue(new Rx.Subject()); setupContract.getUpdateErrors$.mockReturnValue(new Rx.Subject()); + setupContract.getAll.mockReturnValue({}); return setupContract; }; 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 43927337ce574..6a0748a33e724 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -24,7 +24,7 @@ import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { KibanaContext, KibanaDatatable, - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatableColumn, } from 'src/plugins/expressions/public'; import { @@ -66,7 +66,8 @@ export interface RequestHandlerParams { const name = 'esaggs'; -type Context = KibanaContext | null; +type Input = KibanaContext | null; +type Output = Promise; interface Arguments { index: string; @@ -76,8 +77,6 @@ interface Arguments { aggConfigs: string; } -type Return = Promise; - const handleCourierRequest = async ({ searchSource, aggs, @@ -221,12 +220,10 @@ const handleCourierRequest = async ({ return (searchSource as any).tabifiedResponse; }; -export const esaggs = (): ExpressionFunction => ({ +export const esaggs = (): ExpressionFunctionDefinition => ({ name, type: 'kibana_datatable', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('data.functions.esaggs.help', { defaultMessage: 'Run AggConfig aggregation', }), @@ -256,7 +253,7 @@ export const esaggs = (): ExpressionFunction { - const fn = functionWrapper(createInputControlVisFn); + const fn = functionWrapper(createInputControlVisFn()); const visConfig = { controls: [ { diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts index 0482c0d2cbff3..e779c6d344ab5 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts @@ -20,15 +20,11 @@ import { i18n } from '@kbn/i18n'; import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Render, } from '../../../../plugins/expressions/public'; -const name = 'input_control_vis'; - -type Context = KibanaDatatable; - interface Arguments { visConfig: string; } @@ -40,19 +36,15 @@ interface RenderValue { visConfig: VisParams; } -type Return = Promise>; - -export const createInputControlVisFn = (): ExpressionFunction< - typeof name, - Context, +export const createInputControlVisFn = (): ExpressionFunctionDefinition< + 'input_control_vis', + KibanaDatatable, Arguments, - Return + Render > => ({ name: 'input_control_vis', type: 'render', - context: { - types: [], - }, + inputTypes: [], help: i18n.translate('inputControl.function.help', { defaultMessage: 'Input control visualization', }), @@ -63,7 +55,7 @@ export const createInputControlVisFn = (): ExpressionFunction< help: '', }, }, - async fn(context, args) { + fn(input, args) { const params = JSON.parse(args.visConfig); return { type: 'render', diff --git a/src/legacy/core_plugins/interpreter/README.md b/src/legacy/core_plugins/interpreter/README.md index 1a5cefbe0ed81..6d90ce2d5e2eb 100644 --- a/src/legacy/core_plugins/interpreter/README.md +++ b/src/legacy/core_plugins/interpreter/README.md @@ -1,22 +1,2 @@ Interpreter legacy plugin has been migrated to the New Platform. Use `expressions` New Platform plugin instead. - -In the New Platform: - -```ts -class MyPlugin { - setup(core, { expressions }) { - expressions.registerFunction(myFunction); - } - start(core, { expressions }) { - } -} -``` - -In the Legacy Platform: - -```ts -import { npSetup, npStart } from 'ui/new_platform'; - -npSetup.plugins.expressions.registerFunction(myFunction); -``` diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.ts b/src/legacy/core_plugins/interpreter/public/interpreter.ts index 71bce40ba8235..319a2779010c3 100644 --- a/src/legacy/core_plugins/interpreter/public/interpreter.ts +++ b/src/legacy/core_plugins/interpreter/public/interpreter.ts @@ -22,10 +22,7 @@ import 'uiExports/interpreter'; import { register, registryFactory } from '@kbn/interpreter/common'; import { npSetup } from 'ui/new_platform'; import { registries } from './registries'; -import { - ExpressionInterpretWithHandlers, - ExpressionExecutor, -} from '../../../../plugins/expressions/public'; +import { Executor, ExpressionExecutor } from '../../../../plugins/expressions/public'; // Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins // can register without a transpile step. @@ -46,7 +43,7 @@ export const getInterpreter = async () => { }; // TODO: This function will be left behind in the legacy platform. -export const interpretAst: ExpressionInterpretWithHandlers = async (ast, context, handlers) => { +export const interpretAst: Executor['run'] = async (ast, context, handlers) => { const { interpreter } = await getInterpreter(); return await interpreter.interpretAst(ast, context, handlers); }; diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 6e5269e11652f..2cba9fab7be22 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -59,7 +59,7 @@ export function updateLandingPage(version) { } render( - +
diff --git a/src/legacy/core_plugins/kibana/public/management/index.scss b/src/legacy/core_plugins/kibana/public/management/index.scss index fa02bffd2f89b..123580c0b7907 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.scss +++ b/src/legacy/core_plugins/kibana/public/management/index.scss @@ -11,5 +11,5 @@ // Core @import 'management_app'; -@import 'sections/settings/advanced_settings'; +@import '../../../../../plugins/advanced_settings/public/index'; @import 'sections/index_patterns/index'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index.js index 7d3b783db2f76..54717ad003ade 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index.js @@ -17,6 +17,5 @@ * under the License. */ -import './settings'; import './objects'; import './index_patterns'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.tsx.snap deleted file mode 100644 index e76435fdb73b2..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.tsx.snap +++ /dev/null @@ -1,367 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AdvancedSettings should render read-only when saving is disabled 1`] = ` -
- - - - - - - - - - - - - -
- -
-`; - -exports[`AdvancedSettings should render specific setting if given setting key 1`] = ` -
- - - - - - - - - - - - - - - -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.scss b/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.scss deleted file mode 100644 index 6710583cf5c87..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.scss +++ /dev/null @@ -1,23 +0,0 @@ -.mgtAdvancedSettings__field { - + * { - margin-top: $euiSize; - } - - &Wrapper { - width: 640px; - - @include internetExplorerOnly() { - min-height: 1px; - } - } - - &Actions { - padding-top: $euiSizeM; - } - - @include internetExplorerOnly { - &Row { - min-height: 1px; - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.html b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.html deleted file mode 100644 index 2fe8fce08b4ab..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js deleted file mode 100644 index 16d70a9f4ed57..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.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 { management } from 'ui/management'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import { capabilities } from 'ui/capabilities'; -import { I18nContext } from 'ui/i18n'; -import indexTemplate from './index.html'; - -import React from 'react'; -import { AdvancedSettings } from './advanced_settings'; -import { i18n } from '@kbn/i18n'; -import { getBreadcrumbs } from './breadcrumbs'; - -uiRoutes.when('/management/kibana/settings/:setting?', { - template: indexTemplate, - k7Breadcrumbs: getBreadcrumbs, - requireUICapability: 'management.kibana.settings', - badge: uiCapabilities => { - if (uiCapabilities.advancedSettings.save) { - return undefined; - } - - return { - text: i18n.translate('kbn.management.advancedSettings.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('kbn.management.advancedSettings.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save advanced settings', - }), - iconType: 'glasses', - }; - }, -}); - -uiModules.get('apps/management').directive('kbnManagementAdvanced', function($route) { - return { - restrict: 'E', - link: function($scope) { - $scope.query = $route.current.params.setting || ''; - $route.updateParams({ setting: null }); - }, - }; -}); - -const AdvancedSettingsApp = ({ query = '' }) => { - return ( - - - - ); -}; - -uiModules.get('apps/management').directive('kbnManagementAdvancedReact', function(reactDirective) { - return reactDirective(AdvancedSettingsApp, [['query', { watchDepth: 'reference' }]]); -}); - -management.getSection('kibana').register('settings', { - display: i18n.translate('kbn.management.settings.sectionLabel', { - defaultMessage: 'Advanced Settings', - }), - order: 20, - url: '#/management/kibana/settings', -}); diff --git a/src/legacy/core_plugins/region_map/public/region_map_fn.test.js b/src/legacy/core_plugins/region_map/public/region_map_fn.test.js index 4a788793736e8..07b4e33b85e27 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_fn.test.js +++ b/src/legacy/core_plugins/region_map/public/region_map_fn.test.js @@ -18,13 +18,13 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; import { createRegionMapFn } from './region_map_fn'; jest.mock('ui/new_platform'); describe('interpreter/functions#regionmap', () => { - const fn = functionWrapper(createRegionMapFn); + const fn = functionWrapper(createRegionMapFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js index aed8477057165..d32d3e837c0d0 100644 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import { PRIVACY_STATEMENT_URL } from '../../common/constants'; import { OptInExampleFlyout } from './opt_in_details_component'; -import { Field } from '../../../kibana/public/management/sections/settings/components/field/field'; +import { Field } from '../../../../../plugins/advanced_settings/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; diff --git a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js b/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js index d46a5d0df7422..0913d6fc92e8a 100644 --- a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js +++ b/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js @@ -18,7 +18,7 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; import { createTileMapFn } from './tile_map_fn'; jest.mock('ui/new_platform'); @@ -40,7 +40,7 @@ jest.mock('ui/vis/map/convert_to_geojson', () => ({ import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; describe('interpreter/functions#tilemap', () => { - const fn = functionWrapper(createTileMapFn); + const fn = functionWrapper(createTileMapFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts index 44e891ea1ac93..5f41840bac99b 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts @@ -18,11 +18,11 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; import { createMarkdownVisFn } from './markdown_fn'; describe('interpreter/functions#markdown', () => { - const fn = functionWrapper(createMarkdownVisFn); + const fn = functionWrapper(createMarkdownVisFn()); const args = { font: { spec: { fontSize: 12 } }, openLinksInNewTab: true, diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts index 91a0b2ce35604..bbf2b7844c73f 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts @@ -18,31 +18,23 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, Render } from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; import { Arguments, MarkdownVisParams } from './types'; -const name = 'markdownVis'; - -type Context = undefined; - interface RenderValue { visType: 'markdown'; visConfig: MarkdownVisParams; } -type Return = Promise>; - -export const createMarkdownVisFn = (): ExpressionFunction< - typeof name, - Context, +export const createMarkdownVisFn = (): ExpressionFunctionDefinition< + 'markdownVis', + unknown, Arguments, - Return + Render > => ({ - name, + name: 'markdownVis', type: 'render', - context: { - types: [], - }, + inputTypes: [], help: i18n.translate('visTypeMarkdown.function.help', { defaultMessage: 'Markdown visualization', }), @@ -70,7 +62,7 @@ export const createMarkdownVisFn = (): ExpressionFunction< }), }, }, - async fn(context, args) { + fn(input, args) { return { type: 'render', as: 'visualization', diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx index 64abee729f4e7..a93bb618da31f 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -19,13 +19,11 @@ import { last, findIndex, isNaN } from 'lodash'; import React, { Component } from 'react'; - import { isColorDark } from '@elastic/eui'; - import { getFormat } from '../legacy_imports'; import { MetricVisValue } from './metric_vis_value'; +import { Input } from '../metric_vis_fn'; import { FieldFormatsContentType, IFieldFormat } from '../../../../../plugins/data/public'; -import { Context } from '../metric_vis_fn'; import { KibanaDatatable } from '../../../../../plugins/expressions/public'; import { getHeatmapColors } from '../../../../../plugins/charts/public'; import { VisParams, MetricVisMetric } from '../types'; @@ -33,7 +31,7 @@ import { SchemaConfig, Vis } from '../../../visualizations/public'; export interface MetricVisComponentProps { visParams: VisParams; - visData: Context; + visData: Input; vis: Vis; renderComplete: () => void; } diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts index 389b0f53916d0..4094cd4eff060 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -19,12 +19,12 @@ import { createMetricVisFn } from './metric_vis_fn'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; jest.mock('ui/new_platform'); describe('interpreter/functions#metric', () => { - const fn = functionWrapper(createMetricVisFn); + const fn = functionWrapper(createMetricVisFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], 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 644de88021c1f..03b412c6fff15 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 @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Range, Render, @@ -30,9 +30,7 @@ import { ColorModes } from '../../vis_type_vislib/public'; import { visType, DimensionsVisParam, VisParams } from './types'; import { ColorSchemas, vislibColorMaps } from '../../../../plugins/charts/public'; -export type Context = KibanaDatatable; - -const name = 'metricVis'; +export type Input = KibanaDatatable; interface Arguments { percentageMode: boolean; @@ -51,24 +49,20 @@ interface Arguments { interface RenderValue { visType: typeof visType; - visData: Context; + visData: Input; visConfig: Pick; params: any; } -type Return = Render; - -export const createMetricVisFn = (): ExpressionFunction< - typeof name, - Context, +export const createMetricVisFn = (): ExpressionFunctionDefinition< + 'metricVis', + Input, Arguments, - Return + Render > => ({ - name, + name: 'metricVis', type: 'render', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeMetric.function.help', { defaultMessage: 'Metric visualization', }), @@ -165,7 +159,7 @@ export const createMetricVisFn = (): ExpressionFunction< }), }, }, - fn(context: Context, args: Arguments) { + fn(input, args) { const dimensions: DimensionsVisParam = { metrics: args.metric, }; @@ -184,7 +178,7 @@ export const createMetricVisFn = (): ExpressionFunction< type: 'render', as: 'visualization', value: { - visData: context, + visData: input, visType, visConfig: { metric: { diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts index c8a4cade0efcb..36392c10f93f3 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts @@ -21,7 +21,7 @@ import { createTableVisFn } from './table_vis_fn'; import { tableVisResponseHandler } from './table_vis_response_handler'; // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; jest.mock('./table_vis_response_handler', () => ({ tableVisResponseHandler: jest.fn().mockReturnValue({ @@ -30,7 +30,7 @@ jest.mock('./table_vis_response_handler', () => ({ })); describe('interpreter/functions#table', () => { - const fn = functionWrapper(createTableVisFn); + const fn = functionWrapper(createTableVisFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts index 67dd3b7c90335..a97e596e89754 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts @@ -19,16 +19,13 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; - import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Render, } from '../../../../plugins/expressions/public'; -const name = 'kibana_table'; - -export type Context = KibanaDatatable; +export type Input = KibanaDatatable; interface Arguments { visConfig: string | null; @@ -45,19 +42,15 @@ interface RenderValue { }; } -type Return = Render; - -export const createTableVisFn = (): ExpressionFunction< - typeof name, - Context, +export const createTableVisFn = (): ExpressionFunctionDefinition< + 'kibana_table', + Input, Arguments, - Return + Render > => ({ - name, + name: 'kibana_table', type: 'render', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeTable.function.help', { defaultMessage: 'Table visualization', }), @@ -68,9 +61,9 @@ export const createTableVisFn = (): ExpressionFunction< help: '', }, }, - fn(context, args) { + fn(input, args) { const visConfig = args.visConfig && JSON.parse(args.visConfig); - const convertedData = tableVisResponseHandler(context, visConfig.dimensions); + const convertedData = tableVisResponseHandler(input, visConfig.dimensions); return { type: 'render', diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts index c835d5361fc14..426480fa5b52d 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts @@ -20,7 +20,7 @@ import { Required } from '@kbn/utility-types'; import { getFormat } from './legacy_imports'; -import { Context } from './table_vis_fn'; +import { Input } from './table_vis_fn'; export interface TableContext { tables: Array; @@ -29,7 +29,7 @@ export interface TableContext { export interface TableGroup { $parent: TableContext; - table: Context; + table: Input; tables: Table[]; title: string; name: string; @@ -40,11 +40,11 @@ export interface TableGroup { export interface Table { $parent?: TableGroup; - columns: Context['columns']; - rows: Context['rows']; + columns: Input['columns']; + rows: Input['rows']; } -export function tableVisResponseHandler(table: Context, dimensions: any): TableContext { +export function tableVisResponseHandler(table: Input, dimensions: any): TableContext { const converted: TableContext = { tables: [], }; @@ -63,8 +63,7 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC const splitValue: any = row[splitColumn.id]; if (!splitMap.hasOwnProperty(splitValue as any)) { - // @ts-ignore - splitMap[splitValue] = splitIndex++; + (splitMap as any)[splitValue] = splitIndex++; const tableGroup: Required = { $parent: converted, title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, @@ -85,10 +84,8 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC converted.tables.push(tableGroup); } - // @ts-ignore - const tableIndex = splitMap[splitValue]; - // @ts-ignore - converted.tables[tableIndex].tables[0].rows.push(row); + const tableIndex = (splitMap as any)[splitValue]; + (converted.tables[tableIndex] as any).tables[0].rows.push(row); }); } else { converted.tables.push({ diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index 16982a76412e9..65c54766133d1 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -20,10 +20,10 @@ import { createTagCloudFn } from './tag_cloud_fn'; // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#tagcloud', () => { - const fn = functionWrapper(createTagCloudFn); + const fn = functionWrapper(createTagCloudFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 90f952fde3447..31c7fd118cefd 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Render, } from '../../../../plugins/expressions/public'; @@ -28,8 +28,6 @@ import { TagCloudVisParams } from './types'; const name = 'tagcloud'; -type Context = KibanaDatatable; - interface Arguments extends TagCloudVisParams { metric: any; // these aren't typed yet bucket: any; // these aren't typed yet @@ -37,24 +35,20 @@ interface Arguments extends TagCloudVisParams { interface RenderValue { visType: typeof name; - visData: Context; + visData: KibanaDatatable; visConfig: Arguments; params: any; } -type Return = Render; - -export const createTagCloudFn = (): ExpressionFunction< +export const createTagCloudFn = (): ExpressionFunctionDefinition< typeof name, - Context, + KibanaDatatable, Arguments, - Return + Render > => ({ name, type: 'render', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeTagCloud.function.help', { defaultMessage: 'Tagcloud visualization', }), @@ -104,7 +98,7 @@ export const createTagCloudFn = (): ExpressionFunction< }), }, }, - fn(context, args) { + fn(input, args) { const visConfig = { scale: args.scale, orientation: args.orientation, @@ -122,7 +116,7 @@ export const createTagCloudFn = (): ExpressionFunction< type: 'render', as: 'visualization', value: { - visData: context, + visData: input, visType: name, visConfig, params: { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts index 8a517b6cecbc7..c02f43818af9c 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -19,36 +19,36 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaContext, Render } from 'src/plugins/expressions/public'; +import { + ExpressionFunctionDefinition, + KibanaContext, + Render, +} from 'src/plugins/expressions/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TIMELION_VIS_NAME } from './timelion_vis_type'; import { TimelionVisDependencies } from './plugin'; -const name = 'timelion_vis'; - +type Input = KibanaContext | null; +type Output = Promise>; interface Arguments { expression: string; interval: string; } interface RenderValue { - visData: Context; + visData: Input; visType: 'timelion'; visParams: VisParams; } -type Context = KibanaContext | null; export type VisParams = Arguments; -type Return = Promise>; export const getTimelionVisualizationConfig = ( dependencies: TimelionVisDependencies -): ExpressionFunction => ({ - name, +): ExpressionFunctionDefinition<'timelion_vis', Input, Arguments, Output> => ({ + name: 'timelion_vis', type: 'render', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('timelion.function.help', { defaultMessage: 'Timelion visualization', }), @@ -65,15 +65,15 @@ export const getTimelionVisualizationConfig = ( help: '', }, }, - async fn(context, args) { + async fn(input, args) { const timelionRequestHandler = getTimelionRequestHandler(dependencies); const visParams = { expression: args.expression, interval: args.interval }; const response = await timelionRequestHandler({ - timeRange: get(context, 'timeRange'), - query: get(context, 'query'), - filters: get(context, 'filters'), + timeRange: get(input, 'timeRange'), + query: get(input, 'query'), + filters: get(input, 'filters'), visParams, forceFetch: true, }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts index 5786399fc7830..576723bad1e43 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts @@ -19,14 +19,18 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public'; +import { + ExpressionFunctionDefinition, + KibanaContext, + Render, +} from '../../../../plugins/expressions/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; import { PersistedState } from './legacy_imports'; -const name = 'tsvb'; -type Context = KibanaContext | null; +type Input = KibanaContext | null; +type Output = Promise>; interface Arguments { params: string; @@ -38,19 +42,20 @@ type VisParams = Required; interface RenderValue { visType: 'metrics'; - visData: Context; + visData: Input; visConfig: VisParams; uiState: any; } -type Return = Promise>; - -export const createMetricsFn = (): ExpressionFunction => ({ - name, +export const createMetricsFn = (): ExpressionFunctionDefinition< + 'tsvb', + Input, + Arguments, + Output +> => ({ + name: 'tsvb', type: 'render', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('visTypeTimeseries.function.help', { defaultMessage: 'TSVB visualization', }), @@ -71,16 +76,16 @@ export const createMetricsFn = (): ExpressionFunction>; interface Arguments { spec: string; @@ -34,21 +37,17 @@ interface Arguments { export type VisParams = Required; interface RenderValue { - visData: Context; - visType: typeof name; + visData: Input; + visType: 'vega'; visConfig: VisParams; } -type Return = Promise>; - export const createVegaFn = ( dependencies: VegaVisualizationDependencies -): ExpressionFunction => ({ - name, +): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({ + name: 'vega', type: 'render', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('visTypeVega.function.help', { defaultMessage: 'Vega visualization', }), @@ -59,13 +58,13 @@ export const createVegaFn = ( help: '', }, }, - async fn(context, args) { + async fn(input, args) { const vegaRequestHandler = createVegaRequestHandler(dependencies); const response = await vegaRequestHandler({ - timeRange: get(context, 'timeRange'), - query: get(context, 'query'), - filters: get(context, 'filters'), + timeRange: get(input, 'timeRange'), + query: get(input, 'query'), + filters: get(input, 'filters'), visParams: { spec: args.spec }, }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts index 54bd9e93292e2..15c80e4719487 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts @@ -18,7 +18,7 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; +import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; @@ -42,7 +42,7 @@ jest.mock('./vislib/response_handler', () => ({ })); describe('interpreter/functions#pie', () => { - const fn = functionWrapper(createPieVisFn); + const fn = functionWrapper(createPieVisFn()); const context = { type: 'kibana_datatable', rows: [{ 'col-0-1': 0 }], diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts index 5e80e28b7cc6b..452e0be0df3e4 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts @@ -18,19 +18,14 @@ */ import { i18n } from '@kbn/i18n'; - import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Render, } from '../../../../plugins/expressions/public'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; -const name = 'kibana_pie'; - -type Context = KibanaDatatable; - interface Arguments { visConfig: string; } @@ -41,14 +36,15 @@ interface RenderValue { visConfig: VisParams; } -type Return = Render; - -export const createPieVisFn = (): ExpressionFunction => ({ +export const createPieVisFn = (): ExpressionFunctionDefinition< + 'kibana_pie', + KibanaDatatable, + Arguments, + Render +> => ({ name: 'kibana_pie', type: 'render', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeVislib.functions.pie.help', { defaultMessage: 'Pie visualization', }), @@ -59,9 +55,9 @@ export const createPieVisFn = (): ExpressionFunction, void> { createGaugeVisTypeDefinition, createGoalVisTypeDefinition, ]; - const vislibFns = [createVisTypeVislibVisFn, createPieVisFn]; + const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as | VisTypeXyConfigSchema['visTypeXy'] diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index 5e948496ff08a..854b70b04e58a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,19 +18,14 @@ */ import { i18n } from '@kbn/i18n'; - import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, Render, } from '../../../../plugins/expressions/public'; // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; -const name = 'vislib'; - -type Context = KibanaDatatable; - interface Arguments { type: string; visConfig: string; @@ -43,19 +38,15 @@ interface RenderValue { visConfig: VisParams; } -type Return = Render; - -export const createVisTypeVislibVisFn = (): ExpressionFunction< - typeof name, - Context, +export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< + 'vislib', + KibanaDatatable, Arguments, - Return + Render > => ({ name: 'vislib', type: 'render', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { defaultMessage: 'Vislib visualization', }), diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index d3badcc6bdc3f..049dec792ff4d 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -350,7 +350,6 @@ export class VisualizeEmbeddable extends Embeddable ({ help: 'User interface state', }, }, - async fn(context, args, handlers) { + async fn(input, args, { inspectorAdapters }) { const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {}; const schemas = args.schemas ? JSON.parse(args.schemas) : {}; const visType = getTypes().get(args.type || 'histogram') as any; @@ -96,25 +96,25 @@ export const visualization = (): ExpressionFunctionVisualization => ({ const uiState = new PersistedState(uiStateParams); if (typeof visType.requestHandler === 'function') { - context = await visType.requestHandler({ + input = await visType.requestHandler({ partialRows: args.partialRows, metricsAtAllLevels: args.metricsAtAllLevels, index: indexPattern, visParams: visConfigParams, - timeRange: get(context, 'timeRange', null), - query: get(context, 'query', null), - filters: get(context, 'filters', null), + timeRange: get(input, 'timeRange', null), + query: get(input, 'query', null), + filters: get(input, 'filters', null), uiState, - inspectorAdapters: handlers.inspectorAdapters, + inspectorAdapters, queryFilter: getFilterManager(), forceFetch: true, }); } if (typeof visType.responseHandler === 'function') { - if (context.columns) { + if (input.columns) { // assign schemas to aggConfigs - context.columns.forEach((column: any) => { + input.columns.forEach((column: any) => { if (column.aggConfig) { column.aggConfig.aggConfigs.schemas = visType.schemas.all; } @@ -122,21 +122,21 @@ export const visualization = (): ExpressionFunctionVisualization => ({ Object.keys(schemas).forEach(key => { schemas[key].forEach((i: any) => { - if (context.columns[i] && context.columns[i].aggConfig) { - context.columns[i].aggConfig.schema = key; + if (input.columns[i] && input.columns[i].aggConfig) { + input.columns[i].aggConfig.schema = key; } }); }); } - context = await visType.responseHandler(context, visConfigParams.dimensions); + input = await visType.responseHandler(input, visConfigParams.dimensions); } return { type: 'render', as: 'visualization', value: { - visData: context, + visData: input, visType: args.type || '', visConfig: visConfigParams, }, diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index bafb2caba32be..cac9a6daa8df8 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["home"] + "requiredPlugins": ["management"] } diff --git a/src/plugins/expressions/public/create_handlers.ts b/src/plugins/advanced_settings/public/_index.scss similarity index 90% rename from src/plugins/expressions/public/create_handlers.ts rename to src/plugins/advanced_settings/public/_index.scss index 46e85411c5895..f3fe78bf6a9c0 100644 --- a/src/plugins/expressions/public/create_handlers.ts +++ b/src/plugins/advanced_settings/public/_index.scss @@ -17,8 +17,4 @@ * under the License. */ -export function createHandlers() { - return { - environment: 'client', - }; -} + @import './management_app/advanced_settings'; diff --git a/src/plugins/advanced_settings/public/index.ts b/src/plugins/advanced_settings/public/index.ts index 13be36e671f75..db478fa1579e6 100644 --- a/src/plugins/advanced_settings/public/index.ts +++ b/src/plugins/advanced_settings/public/index.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { AdvancedSettingsPlugin } from './plugin'; export { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; export { ComponentRegistry } from './component_registry'; +export { Field } from './management_app/components/field'; export function plugin(initializerContext: PluginInitializerContext) { return new AdvancedSettingsPlugin(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/breadcrumbs.ts b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss similarity index 72% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/breadcrumbs.ts rename to src/plugins/advanced_settings/public/management_app/advanced_settings.scss index c27b6be1631a9..79b6feccb6b7d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/breadcrumbs.ts +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss @@ -17,16 +17,26 @@ * under the License. */ -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { i18n } from '@kbn/i18n'; +.mgtAdvancedSettings__field { + + * { + margin-top: $euiSize; + } -export function getBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('kbn.management.settings.breadcrumb', { - defaultMessage: 'Advanced settings', - }), - }, - ]; + &Wrapper { + width: 640px; + + @include internetExplorerOnly() { + min-height: 1px; + } + } + + &Actions { + padding-top: $euiSizeM; + } + + @include internetExplorerOnly { + &Row { + min-height: 1px; + } + } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx similarity index 80% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.tsx rename to src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 00b587c2e0fb5..7a2ab648ec258 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -19,17 +19,14 @@ import React from 'react'; import { Observable } from 'rxjs'; -import { shallow } from 'enzyme'; +import { ReactWrapper } from 'enzyme'; +import { mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import dedent from 'dedent'; -import { - UiSettingsParams, - UserProvidedValues, - UiSettingsType, -} from '../../../../../../../core/public'; +import { UiSettingsParams, UserProvidedValues, UiSettingsType } from '../../../../core/public'; import { FieldSetting } from './types'; - -import { AdvancedSettings } from './advanced_settings'; -jest.mock('ui/new_platform'); +import { AdvancedSettingsComponent } from './advanced_settings'; +import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; +import { ComponentRegistry } from '../component_registry'; jest.mock('ui/new_platform', () => ({ npStart: mockConfig(), @@ -219,8 +216,7 @@ function mockConfig() { }, plugins: { advancedSettings: { - component: { - register: jest.fn(), + componentRegistry: { get: () => { const foo: React.ComponentType = () =>
Hello
; foo.displayName = 'foo_component'; @@ -238,18 +234,47 @@ function mockConfig() { describe('AdvancedSettings', () => { it('should render specific setting if given setting key', async () => { - const component = shallow( - + const component = mountWithI18nProvider( + ); - expect(component).toMatchSnapshot(); + expect( + component + .find('Field') + .filterWhere( + (n: ReactWrapper) => + (n.prop('setting') as Record).name === 'test:string:setting' + ) + ).toHaveLength(1); }); it('should render read-only when saving is disabled', async () => { - const component = shallow( - + const component = mountWithI18nProvider( + ); - expect(component).toMatchSnapshot(); + expect( + component + .find('Field') + .filterWhere( + (n: ReactWrapper) => + (n.prop('setting') as Record).name === 'test:string:setting' + ) + .prop('enableSaving') + ).toBe(false); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx similarity index 76% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.tsx rename to src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index c995b391d3d2d..5057d072e3e41 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -18,22 +18,38 @@ */ import React, { Component } from 'react'; -import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; - -import { npStart } from 'ui/new_platform'; +import { Subscription } from 'rxjs'; +import { + Comparators, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + // @ts-ignore + Query, +} from '@elastic/eui'; + +import { useParams } from 'react-router-dom'; import { CallOuts } from './components/call_outs'; import { Search } from './components/search'; import { Form } from './components/form'; import { AdvancedSettingsVoiceAnnouncement } from './components/advanced_settings_voice_announcement'; -import { IUiSettingsClient } from '../../../../../../../core/public/'; +import { IUiSettingsClient, DocLinksStart, ToastsStart } from '../../../../core/public/'; +import { ComponentRegistry } from '../'; import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; import { FieldSetting, IQuery } from './types'; interface AdvancedSettingsProps { - queryText: string; enableSaving: boolean; + uiSettings: IUiSettingsClient; + dockLinks: DocLinksStart['links']; + toasts: ToastsStart; + componentRegistry: ComponentRegistry['start']; +} + +interface AdvancedSettingsComponentProps extends AdvancedSettingsProps { + queryText: string; } interface AdvancedSettingsState { @@ -44,24 +60,25 @@ interface AdvancedSettingsState { type GroupedSettings = Record; -export class AdvancedSettings extends Component { - private config: IUiSettingsClient; +export class AdvancedSettingsComponent extends Component< + AdvancedSettingsComponentProps, + AdvancedSettingsState +> { private settings: FieldSetting[]; private groupedSettings: GroupedSettings; private categoryCounts: Record; private categories: string[] = []; + private uiSettingsSubscription?: Subscription; - constructor(props: AdvancedSettingsProps) { + constructor(props: AdvancedSettingsComponentProps) { super(props); - const { queryText } = this.props; - const parsedQuery = Query.parse(queryText ? `ariaName:"${getAriaName(queryText)}"` : ''); - this.config = npStart.core.uiSettings; - this.settings = this.initSettings(this.config); + this.settings = this.initSettings(this.props.uiSettings); this.groupedSettings = this.initGroupedSettings(this.settings); this.categories = this.initCategories(this.groupedSettings); this.categoryCounts = this.initCategoryCounts(this.groupedSettings); + const parsedQuery = Query.parse(this.props.queryText ? getAriaName(this.props.queryText) : ''); this.state = { query: parsedQuery, footerQueryMatched: false, @@ -97,15 +114,21 @@ export class AdvancedSettings extends Component { + this.uiSettingsSubscription = this.props.uiSettings.getUpdate$().subscribe(() => { const { query } = this.state; - this.init(this.config); + this.init(this.props.uiSettings); this.setState({ filteredSettings: this.mapSettings(Query.execute(query, this.settings)), }); }); } + componentWillUnmount() { + if (this.uiSettingsSubscription) { + this.uiSettingsSubscription.unsubscribe(); + } + } + mapConfig(config: IUiSettingsClient) { const all = config.getAll(); return Object.entries(all) @@ -156,7 +179,7 @@ export class AdvancedSettings extends Component { + const { query } = useParams(); + return ( + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap similarity index 87% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap rename to src/plugins/advanced_settings/public/management_app/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap index e8c8184cf7e57..490e105c18a7d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/advanced_settings_voice_announcement/__snapshots__/advanced_settings_voice_announcement.test.tsx.snap @@ -11,7 +11,7 @@ exports[`Advanced Settings: Voice Announcement should render announcement 1`] = > {
} @@ -16,7 +16,7 @@ exports[`CallOuts should render normally 1`] = `

diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.test.tsx b/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.test.tsx rename to src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.tsx b/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.tsx rename to src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx index cbd2bcfeb5454..3c6b4a51ed540 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx @@ -28,7 +28,7 @@ export const CallOuts = () => { } @@ -37,7 +37,7 @@ export const CallOuts = () => { >

@@ -129,7 +129,7 @@ exports[`Field for array setting should render as read only with help text if ov > @@ -185,7 +185,7 @@ exports[`Field for array setting should render custom setting icon if it is cust content={ } @@ -319,7 +319,7 @@ exports[`Field for array setting should render user value if there is user value @@ -362,7 +362,7 @@ exports[`Field for array setting should render user value if there is user value > @@ -444,7 +444,7 @@ exports[`Field for boolean setting should render as read only if saving is disab label={ } @@ -488,7 +488,7 @@ exports[`Field for boolean setting should render as read only with help text if @@ -527,7 +527,7 @@ exports[`Field for boolean setting should render as read only with help text if > @@ -544,7 +544,7 @@ exports[`Field for boolean setting should render as read only with help text if label={ } @@ -589,7 +589,7 @@ exports[`Field for boolean setting should render custom setting icon if it is cu content={ } @@ -623,7 +623,7 @@ exports[`Field for boolean setting should render custom setting icon if it is cu label={ } @@ -691,7 +691,7 @@ exports[`Field for boolean setting should render default value if there is no us label={ } @@ -735,7 +735,7 @@ exports[`Field for boolean setting should render user value if there is user val @@ -778,7 +778,7 @@ exports[`Field for boolean setting should render user value if there is user val > @@ -798,7 +798,7 @@ exports[`Field for boolean setting should render user value if there is user val label={ } @@ -905,7 +905,7 @@ exports[`Field for image setting should render as read only with help text if ov @@ -944,7 +944,7 @@ exports[`Field for image setting should render as read only with help text if ov > @@ -997,7 +997,7 @@ exports[`Field for image setting should render custom setting icon if it is cust content={ } @@ -1133,7 +1133,7 @@ exports[`Field for image setting should render user value if there is user value @@ -1176,7 +1176,7 @@ exports[`Field for image setting should render user value if there is user value > @@ -1190,7 +1190,7 @@ exports[`Field for image setting should render user value if there is user value > @@ -1244,7 +1244,7 @@ exports[`Field for json setting should render as read only if saving is disabled @@ -1467,7 +1467,7 @@ exports[`Field for json setting should render custom setting icon if it is custo content={ } @@ -1558,7 +1558,7 @@ exports[`Field for json setting should render default value if there is no user @@ -1681,7 +1681,7 @@ exports[`Field for json setting should render user value if there is user value @@ -1885,7 +1885,7 @@ exports[`Field for markdown setting should render as read only with help text if @@ -1924,7 +1924,7 @@ exports[`Field for markdown setting should render as read only with help text if > @@ -1999,7 +1999,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c content={ } @@ -2171,7 +2171,7 @@ exports[`Field for markdown setting should render user value if there is user va @@ -2214,7 +2214,7 @@ exports[`Field for markdown setting should render user value if there is user va > @@ -2353,7 +2353,7 @@ exports[`Field for number setting should render as read only with help text if o @@ -2392,7 +2392,7 @@ exports[`Field for number setting should render as read only with help text if o > @@ -2448,7 +2448,7 @@ exports[`Field for number setting should render custom setting icon if it is cus content={ } @@ -2582,7 +2582,7 @@ exports[`Field for number setting should render user value if there is user valu @@ -2625,7 +2625,7 @@ exports[`Field for number setting should render user value if there is user valu > @@ -2761,7 +2761,7 @@ exports[`Field for select setting should render as read only with help text if o @@ -2800,7 +2800,7 @@ exports[`Field for select setting should render as read only with help text if o > @@ -2872,7 +2872,7 @@ exports[`Field for select setting should render custom setting icon if it is cus content={ } @@ -3038,7 +3038,7 @@ exports[`Field for select setting should render user value if there is user valu @@ -3081,7 +3081,7 @@ exports[`Field for select setting should render user value if there is user valu > @@ -3217,7 +3217,7 @@ exports[`Field for string setting should render as read only with help text if o @@ -3256,7 +3256,7 @@ exports[`Field for string setting should render as read only with help text if o > @@ -3312,7 +3312,7 @@ exports[`Field for string setting should render custom setting icon if it is cus content={ } @@ -3446,7 +3446,7 @@ exports[`Field for string setting should render user value if there is user valu @@ -3489,7 +3489,7 @@ exports[`Field for string setting should render user value if there is user valu > @@ -3609,7 +3609,7 @@ exports[`Field for stringWithValidation setting should render as read only with @@ -3648,7 +3648,7 @@ exports[`Field for stringWithValidation setting should render as read only with > @@ -3704,7 +3704,7 @@ exports[`Field for stringWithValidation setting should render custom setting ico content={ } @@ -3838,7 +3838,7 @@ exports[`Field for stringWithValidation setting should render user value if ther @@ -3881,7 +3881,7 @@ exports[`Field for stringWithValidation setting should render user value if ther > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx similarity index 88% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.tsx rename to src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index bd2ba8ac0ebcc..81df22ccf6e43 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -22,7 +22,8 @@ import { I18nProvider } from '@kbn/i18n/react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { mount } from 'enzyme'; import { FieldSetting } from '../../types'; -import { UiSettingsType, StringValidation } from '../../../../../../../../../core/public'; +import { UiSettingsType, StringValidation } from '../../../../../../core/public'; +import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; @@ -35,8 +36,6 @@ jest.mock('ui/notify', () => ({ }, })); -import { toastNotifications } from 'ui/notify'; - jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); jest.mock('brace/mode/markdown', () => 'brace/mode/markdown'); @@ -196,7 +195,14 @@ describe('Field', () => { describe(`for ${type} setting`, () => { it('should render default value if there is no user value set', async () => { const component = shallowWithI18nProvider( - + ); expect(component).toMatchSnapshot(); @@ -214,6 +220,8 @@ describe('Field', () => { save={save} clear={clear} enableSaving={true} + toasts={notificationServiceMock.createStartContract().toasts} + dockLinks={docLinksServiceMock.createStartContract().links} /> ); @@ -222,7 +230,14 @@ describe('Field', () => { it('should render as read only if saving is disabled', async () => { const component = shallowWithI18nProvider( - + ); expect(component).toMatchSnapshot(); @@ -239,6 +254,8 @@ describe('Field', () => { save={save} clear={clear} enableSaving={true} + toasts={notificationServiceMock.createStartContract().toasts} + dockLinks={docLinksServiceMock.createStartContract().links} /> ); @@ -255,6 +272,8 @@ describe('Field', () => { save={save} clear={clear} enableSaving={true} + toasts={notificationServiceMock.createStartContract().toasts} + dockLinks={docLinksServiceMock.createStartContract().links} /> ); @@ -273,6 +292,8 @@ describe('Field', () => { save={save} clear={clear} enableSaving={true} + toasts={notificationServiceMock.createStartContract().toasts} + dockLinks={docLinksServiceMock.createStartContract().links} /> ); const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); @@ -291,6 +312,8 @@ describe('Field', () => { save={save} clear={clear} enableSaving={true} + toasts={notificationServiceMock.createStartContract().toasts} + dockLinks={docLinksServiceMock.createStartContract().links} /> ); const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); @@ -303,7 +326,15 @@ describe('Field', () => { const setup = () => { const Wrapper = (props: Record) => ( - + ); const wrapper = mount(); @@ -489,15 +520,23 @@ describe('Field', () => { ...settings.string, requiresPageReload: true, }; + const toasts = notificationServiceMock.createStartContract().toasts; const wrapper = mountWithI18nProvider( - + ); (wrapper.instance() as Field).onFieldChange({ target: { value: 'a new value' } }); const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); expect(save).toHaveBeenCalled(); await save(); - expect(toastNotifications.add).toHaveBeenCalledWith( + expect(toasts.add).toHaveBeenCalledWith( expect.objectContaining({ title: expect.stringContaining('Please reload the page'), }) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx similarity index 87% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.tsx rename to src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 666efc0762aed..5cf5ea99faa80 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -19,19 +19,19 @@ import React, { PureComponent, Fragment } from 'react'; import ReactDOM from 'react-dom'; -import { npStart } from 'ui/new_platform'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; -import { toastNotifications } from 'ui/notify'; import { EuiBadge, EuiButton, EuiButtonEmpty, EuiCode, EuiCodeBlock, + // @ts-ignore EuiCodeEditor, + // @ts-ignore EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, @@ -59,13 +59,17 @@ import { UiSettingsType, ImageValidation, StringValidationRegex, -} from '../../../../../../../../../core/public'; + DocLinksStart, + ToastsStart, +} from '../../../../../../core/public'; interface FieldProps { setting: FieldSetting; save: (name: string, value: string) => Promise; clear: (name: string) => Promise; enableSaving: boolean; + dockLinks: DocLinksStart['links']; + toasts: ToastsStart; } interface FieldState { @@ -175,7 +179,7 @@ export class Field extends PureComponent { JSON.parse(newUnsavedValue); } catch (e) { isInvalid = true; - error = i18n.translate('kbn.management.settings.field.codeEditorSyntaxErrorMessage', { + error = i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { defaultMessage: 'Invalid JSON syntax', }); } @@ -267,7 +271,7 @@ export class Field extends PureComponent { this.setState({ isInvalid, error: isInvalid - ? i18n.translate('kbn.management.settings.field.imageTooLargeErrorMessage', { + ? i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', values: { maxSizeDescription: maxSize.description, @@ -278,8 +282,8 @@ export class Field extends PureComponent { unsavedValue: base64Image, }); } catch (err) { - toastNotifications.addDanger( - i18n.translate('kbn.management.settings.field.imageChangeErrorMessage', { + this.props.toasts.addDanger( + i18n.translate('advancedSettings.field.imageChangeErrorMessage', { defaultMessage: 'Image could not be saved', }) ); @@ -331,8 +335,8 @@ export class Field extends PureComponent { showPageReloadToast = () => { if (this.props.setting.requiresPageReload) { - toastNotifications.add({ - title: i18n.translate('kbn.management.settings.field.requiresPageReloadToastDescription', { + this.props.toasts.add({ + title: i18n.translate('advancedSettings.field.requiresPageReloadToastDescription', { defaultMessage: 'Please reload the page for the "{settingName}" setting to take effect.', values: { settingName: this.props.setting.displayName || this.props.setting.name, @@ -344,10 +348,9 @@ export class Field extends PureComponent { window.location.reload()}> - {i18n.translate( - 'kbn.management.settings.field.requiresPageReloadToastButtonLabel', - { defaultMessage: 'Reload page' } - )} + {i18n.translate('advancedSettings.field.requiresPageReloadToastButtonLabel', { + defaultMessage: 'Reload page', + })} @@ -398,8 +401,8 @@ export class Field extends PureComponent { this.cancelChangeImage(); } } catch (e) { - toastNotifications.addDanger( - i18n.translate('kbn.management.settings.field.saveFieldErrorMessage', { + this.props.toasts.addDanger( + i18n.translate('advancedSettings.field.saveFieldErrorMessage', { defaultMessage: 'Unable to save {name}', values: { name }, }) @@ -417,8 +420,8 @@ export class Field extends PureComponent { this.cancelChangeImage(); this.clearError(); } catch (e) { - toastNotifications.addDanger( - i18n.translate('kbn.management.settings.field.resetFieldErrorMessage', { + this.props.toasts.addDanger( + i18n.translate('advancedSettings.field.resetFieldErrorMessage', { defaultMessage: 'Unable to reset {name}', values: { name }, }) @@ -438,12 +441,9 @@ export class Field extends PureComponent { + ) : ( - + ) } checked={!!unsavedValue} @@ -553,7 +553,7 @@ export class Field extends PureComponent { return ( @@ -584,12 +584,12 @@ export class Field extends PureComponent { } @@ -606,7 +606,7 @@ export class Field extends PureComponent { let deprecation; if (setting.deprecation) { - const { links } = npStart.core.docLinks; + const links = this.props.dockLinks; deprecation = ( <> @@ -616,15 +616,12 @@ export class Field extends PureComponent { onClick={() => { window.open(links.management[setting.deprecation!.docLinksKey], '_blank'); }} - onClickAriaLabel={i18n.translate( - 'kbn.management.settings.field.deprecationClickAreaLabel', - { - defaultMessage: 'Click to view deprecation documentation for {settingName}.', - values: { - settingName: setting.name, - }, - } - )} + onClickAriaLabel={i18n.translate('advancedSettings.field.deprecationClickAreaLabel', { + defaultMessage: 'Click to view deprecation documentation for {settingName}.', + values: { + settingName: setting.name, + }, + })} > Deprecated @@ -669,7 +666,7 @@ export class Field extends PureComponent { {type === 'json' ? ( { ) : ( { return ( { data-test-subj={`advancedSetting-resetField-${name}`} > @@ -738,7 +735,7 @@ export class Field extends PureComponent { return ( { data-test-subj={`advancedSetting-changeImage-${name}`} > @@ -771,7 +768,7 @@ export class Field extends PureComponent { { disabled={isDisabled || isInvalid} data-test-subj={`advancedSetting-saveEditField-${name}`} > - + (changeImage ? this.cancelChangeImage() : this.cancelEdit())} disabled={isDisabled} data-test-subj={`advancedSetting-cancelEditField-${name}`} > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/index.ts rename to src/plugins/advanced_settings/public/management_app/components/field/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.tsx.snap rename to src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap index b43c17c2a8865..8c471f5f5be9c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Form should render no settings message when there are no settings 1`] = > , @@ -52,6 +52,7 @@ exports[`Form should render normally 1`] = ` /> @@ -125,6 +129,7 @@ exports[`Form should render normally 1`] = ` /> @@ -173,7 +179,7 @@ exports[`Form should render normally 1`] = ` @@ -200,6 +206,7 @@ exports[`Form should render normally 1`] = ` /> @@ -254,6 +262,7 @@ exports[`Form should render read-only when saving is disabled 1`] = ` /> @@ -327,6 +339,7 @@ exports[`Form should render read-only when saving is disabled 1`] = ` /> @@ -375,7 +389,7 @@ exports[`Form should render read-only when saving is disabled 1`] = ` @@ -402,6 +416,7 @@ exports[`Form should render read-only when saving is disabled 1`] = ` /> diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.test.tsx rename to src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 6bbcfd543a629..468cfbfc70820 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { UiSettingsType } from '../../../../../../../../../core/public'; +import { UiSettingsType } from '../../../../../../core/public'; import { Form } from './form'; @@ -101,6 +101,8 @@ describe('Form', () => { clearQuery={clearQuery} showNoResultsMessage={true} enableSaving={true} + toasts={{} as any} + dockLinks={{} as any} /> ); @@ -118,6 +120,8 @@ describe('Form', () => { clearQuery={clearQuery} showNoResultsMessage={true} enableSaving={false} + toasts={{} as any} + dockLinks={{} as any} /> ); @@ -135,6 +139,8 @@ describe('Form', () => { clearQuery={clearQuery} showNoResultsMessage={true} enableSaving={true} + toasts={{} as any} + dockLinks={{} as any} /> ); @@ -152,6 +158,8 @@ describe('Form', () => { clearQuery={clearQuery} showNoResultsMessage={false} enableSaving={true} + toasts={{} as any} + dockLinks={{} as any} /> ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx similarity index 90% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.tsx rename to src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 113e0b2db5f30..91d587866836e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -29,6 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; import { Field } from '../field'; @@ -45,6 +46,8 @@ interface FormProps { clear: (key: string) => Promise; showNoResultsMessage: boolean; enableSaving: boolean; + dockLinks: DocLinksStart['links']; + toasts: ToastsStart; } export class Form extends PureComponent { @@ -56,7 +59,7 @@ export class Form extends PureComponent { { @@ -102,6 +105,8 @@ export class Form extends PureComponent { save={this.props.save} clear={this.props.clear} enableSaving={this.props.enableSaving} + dockLinks={this.props.dockLinks} + toasts={this.props.toasts} /> ); })} @@ -117,13 +122,13 @@ export class Form extends PureComponent { return ( diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/index.ts b/src/plugins/advanced_settings/public/management_app/components/form/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/form/index.ts rename to src/plugins/advanced_settings/public/management_app/components/form/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/__snapshots__/search.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/search/__snapshots__/search.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/__snapshots__/search.test.tsx.snap rename to src/plugins/advanced_settings/public/management_app/components/search/__snapshots__/search.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/index.ts b/src/plugins/advanced_settings/public/management_app/components/search/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/index.ts rename to src/plugins/advanced_settings/public/management_app/components/search/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/search.test.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/search.test.tsx rename to src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/search.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/search.tsx rename to src/plugins/advanced_settings/public/management_app/components/search/search.tsx index 471f2ba28005c..51402296a44a2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/search/search.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx @@ -75,7 +75,7 @@ export class Search extends PureComponent { const box = { incremental: true, 'data-test-subj': 'settingsSearchBar', - 'aria-label': i18n.translate('kbn.management.settings.searchBarAriaLabel', { + 'aria-label': i18n.translate('advancedSettings.searchBarAriaLabel', { defaultMessage: 'Search advanced settings', }), // hack until EuiSearchBar is fixed }; @@ -84,7 +84,7 @@ export class Search extends PureComponent { { type: 'field_value_selection', field: 'category', - name: i18n.translate('kbn.management.settings.categorySearchLabel', { + name: i18n.translate('advancedSettings.categorySearchLabel', { defaultMessage: 'Category', }), multiSelect: 'or', @@ -95,7 +95,7 @@ export class Search extends PureComponent { let queryParseError; if (!this.state.isSearchTextValid) { const parseErrorMsg = i18n.translate( - 'kbn.management.settings.searchBar.unableToParseQueryErrorMessage', + 'advancedSettings.searchBar.unableToParseQueryErrorMessage', { defaultMessage: 'Unable to parse query' } ); queryParseError = ( diff --git a/src/plugins/advanced_settings/public/management_app/index.tsx b/src/plugins/advanced_settings/public/management_app/index.tsx new file mode 100644 index 0000000000000..27d3114051c16 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/index.tsx @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { AdvancedSettings } from './advanced_settings'; +import { ManagementSetup } from '../../../management/public'; +import { CoreSetup } from '../../../../core/public'; +import { ComponentRegistry } from '../types'; + +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); +const crumb = [{ text: title }]; + +const readOnlyBadge = { + text: i18n.translate('advancedSettings.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save advanced settings', + }), + iconType: 'glasses', +}; + +export async function registerAdvSettingsMgmntApp({ + management, + getStartServices, + componentRegistry, +}: { + management: ManagementSetup; + getStartServices: CoreSetup['getStartServices']; + componentRegistry: ComponentRegistry['start']; +}) { + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + + const advancedSettingsManagementApp = kibanaSection.registerApp({ + id: 'settings', + title, + order: 20, + async mount(params) { + params.setBreadcrumbs(crumb); + const [ + { uiSettings, notifications, docLinks, application, chrome }, + ] = await getStartServices(); + + const canSave = application.capabilities.advancedSettings.save as boolean; + + if (!canSave) { + chrome.setBadge(readOnlyBadge); + } + + ReactDOM.render( + + + + + + + + + , + params.element + ); + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }); + const [{ application }] = await getStartServices(); + if (!application.capabilities.management.kibana.settings) { + advancedSettingsManagementApp.disable(); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/default_category.ts b/src/plugins/advanced_settings/public/management_app/lib/default_category.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/default_category.ts rename to src/plugins/advanced_settings/public/management_app/lib/default_category.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_aria_name.test.ts b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_aria_name.test.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_aria_name.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_aria_name.ts b/src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_aria_name.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_aria_name.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.test.ts b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.test.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_category_name.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.ts b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts similarity index 65% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts index d0361ba698eeb..46d28ce9d5c40 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts @@ -22,31 +22,31 @@ import { i18n } from '@kbn/i18n'; const upperFirst = (str = '') => str.replace(/^./, strng => strng.toUpperCase()); const names: Record = { - general: i18n.translate('kbn.management.settings.categoryNames.generalLabel', { + general: i18n.translate('advancedSettings.categoryNames.generalLabel', { defaultMessage: 'General', }), - timelion: i18n.translate('kbn.management.settings.categoryNames.timelionLabel', { + timelion: i18n.translate('advancedSettings.categoryNames.timelionLabel', { defaultMessage: 'Timelion', }), - notifications: i18n.translate('kbn.management.settings.categoryNames.notificationsLabel', { + notifications: i18n.translate('advancedSettings.categoryNames.notificationsLabel', { defaultMessage: 'Notifications', }), - visualizations: i18n.translate('kbn.management.settings.categoryNames.visualizationsLabel', { + visualizations: i18n.translate('advancedSettings.categoryNames.visualizationsLabel', { defaultMessage: 'Visualizations', }), - discover: i18n.translate('kbn.management.settings.categoryNames.discoverLabel', { + discover: i18n.translate('advancedSettings.categoryNames.discoverLabel', { defaultMessage: 'Discover', }), - dashboard: i18n.translate('kbn.management.settings.categoryNames.dashboardLabel', { + dashboard: i18n.translate('advancedSettings.categoryNames.dashboardLabel', { defaultMessage: 'Dashboard', }), - reporting: i18n.translate('kbn.management.settings.categoryNames.reportingLabel', { + reporting: i18n.translate('advancedSettings.categoryNames.reportingLabel', { defaultMessage: 'Reporting', }), - search: i18n.translate('kbn.management.settings.categoryNames.searchLabel', { + search: i18n.translate('advancedSettings.categoryNames.searchLabel', { defaultMessage: 'Search', }), - siem: i18n.translate('kbn.management.settings.categoryNames.siemLabel', { + siem: i18n.translate('advancedSettings.categoryNames.siemLabel', { defaultMessage: 'SIEM', }), }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_val_type.test.ts b/src/plugins/advanced_settings/public/management_app/lib/get_val_type.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_val_type.test.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_val_type.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_val_type.ts b/src/plugins/advanced_settings/public/management_app/lib/get_val_type.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_val_type.ts rename to src/plugins/advanced_settings/public/management_app/lib/get_val_type.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/index.ts b/src/plugins/advanced_settings/public/management_app/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/index.ts rename to src/plugins/advanced_settings/public/management_app/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.test.ts b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.test.ts rename to src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts index 30531ca89b0b5..836dcb6b87676 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.test.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import { isDefaultValue } from './is_default_value'; -import { UiSettingsType } from '../../../../../../../../core/public'; +import { UiSettingsType } from '../../../../../core/public'; describe('Settings', function() { describe('Advanced', function() { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.ts b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.ts rename to src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.test.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.test.ts rename to src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.ts rename to src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/management/sections/settings/types.ts rename to src/plugins/advanced_settings/public/management_app/types.ts index fea70110f6071..05bb5e754563d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -22,7 +22,7 @@ import { StringValidation, ImageValidation, SavedObjectAttribute, -} from '../../../../../../../core/public'; +} from '../../../../core/public'; export interface FieldSetting { displayName: string; diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index bffd5a5157615..e9472fbdee0e6 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -17,29 +17,20 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { ComponentRegistry } from './component_registry'; -import { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; -import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; +import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; +import { registerAdvSettingsMgmntApp } from './management_app'; const component = new ComponentRegistry(); export class AdvancedSettingsPlugin - implements Plugin { - public setup(core: CoreSetup, { home }: { home: HomePublicPluginSetup }) { - home.featureCatalogue.register({ - id: 'advanced_settings', - title: i18n.translate('advancedSettings.advancedSettingsLabel', { - defaultMessage: 'Advanced Settings', - }), - description: i18n.translate('advancedSettings.advancedSettingsDescription', { - defaultMessage: 'Directly edit settings that control behavior in Kibana.', - }), - icon: 'advancedSettingsApp', - path: '/app/kibana#/management/kibana/settings', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, + implements Plugin { + public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { + registerAdvSettingsMgmntApp({ + management, + getStartServices: core.getStartServices, + componentRegistry: component.start, }); return { diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index a9b965c3c22de..a233b3debab8d 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -18,6 +18,7 @@ */ import { ComponentRegistry } from './component_registry'; +import { ManagementSetup } from '../../management/public'; export interface AdvancedSettingsSetup { component: ComponentRegistry['setup']; @@ -25,3 +26,9 @@ export interface AdvancedSettingsSetup { export interface AdvancedSettingsStart { component: ComponentRegistry['start']; } + +export interface AdvancedSettingsPluginSetup { + management: ManagementSetup; +} + +export { ComponentRegistry }; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index fd8f286a9d8f6..5f06e4ec44787 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -29,12 +29,18 @@ import { ContactCardEmbeddable } from '../../../../test_samples/embeddables/cont import { ContainerInput } from '../../../../containers'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; - +import { coreMock } from '../../../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -// eslint-disable-next-line -import { coreMock } from '../../../../../../../../core/public/mocks'; +function DummySavedObjectFinder(props: { children: React.ReactNode }) { + return ( +

+
Hello World
+ {props.children} +
+ ) as JSX.Element; +} test('createNewEmbeddable() add embeddable to container', async () => { const core = coreMock.createStart(); @@ -101,14 +107,14 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' }; const container = new HelloWorldContainer(input, { getEmbeddableFactory } as any); const onClose = jest.fn(); - const component = mount( + const component = mount( new Set([contactCardEmbeddableFactory]).values()} notifications={core.notifications} - SavedObjectFinder={() => null} + SavedObjectFinder={props => } /> ) as ReactWrapper; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 4f2ae7ab19bcb..815394ebd97e0 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -18,24 +18,21 @@ */ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { ReactElement } from 'react'; import { CoreSetup } from 'src/core/public'; import { - EuiButton, EuiContextMenuItem, - EuiContextMenuPanel, EuiFlyout, EuiFlyoutBody, - EuiFlyoutFooter, EuiFlyoutHeader, - EuiPopover, EuiTitle, } from '@elastic/eui'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; import { GetEmbeddableFactories, GetEmbeddableFactory } from '../../../../types'; +import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; interface Props { onClose: () => void; @@ -107,15 +104,7 @@ export class AddPanelFlyout extends React.Component { this.showToast(name); }; - private toggleCreateMenu = () => { - this.setState(prevState => ({ isCreateMenuOpen: !prevState.isCreateMenuOpen })); - }; - - private closeCreateMenu = () => { - this.setState({ isCreateMenuOpen: false }); - }; - - private getCreateMenuItems() { + private getCreateMenuItems(): ReactElement[] { return [...this.props.getAllFactories()] .filter(factory => factory.isEditable() && !factory.isContainerType && factory.canCreateNew()) .map(factory => ( @@ -145,7 +134,9 @@ export class AddPanelFlyout extends React.Component { noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', { defaultMessage: 'No matching objects found.', })} - /> + > + + ); return ( @@ -158,30 +149,6 @@ export class AddPanelFlyout extends React.Component { {savedObjectsFinder} - - - - - } - isOpen={this.state.isCreateMenuOpen} - closePopover={this.closeCreateMenu} - panelPaddingSize="none" - anchorPosition="upLeft" - > - - - ); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx new file mode 100644 index 0000000000000..ac39eacab287f --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx @@ -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 React, { ReactElement, useState } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { EuiContextMenuPanel } from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + menuItems: ReactElement[]; +} + +export function SavedObjectFinderCreateNew({ menuItems }: Props) { + const [isCreateMenuOpen, setCreateMenuOpen] = useState(false); + const toggleCreateMenu = () => { + setCreateMenuOpen(!isCreateMenuOpen); + }; + const closeCreateMenu = () => { + setCreateMenuOpen(false); + }; + return ( + + + + } + isOpen={isCreateMenuOpen} + closePopover={closeCreateMenu} + panelPaddingSize="none" + anchorPosition="downRight" + > + + + ); +} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx new file mode 100644 index 0000000000000..6275dbd4eaa45 --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.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 { SavedObjectFinderCreateNew } from '../saved_object_finder_create_new'; +import { shallow } from 'enzyme'; +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +describe('SavedObjectFinderCreateNew', () => { + test('renders correctly with no items', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiPopover).length).toEqual(1); + const menuPanel = wrapper.find(EuiContextMenuPanel); + expect(menuPanel.length).toEqual(1); + const panelItems = menuPanel.prop('items'); + if (panelItems) { + expect(panelItems.length).toEqual(0); + } else { + fail('Expect paneltems to be defined'); + } + }); + + test('renders correctly with items', () => { + const items = []; + const onClick = jest.fn(); + for (let i = 0; i < 3; i++) { + items.push( + {`item${i + 1}`} + ); + } + + const wrapper = shallow(); + expect(wrapper.find(EuiPopover).length).toEqual(1); + const menuPanel = wrapper.find(EuiContextMenuPanel); + expect(menuPanel.length).toEqual(1); + const paneltems = menuPanel.prop('items'); + if (paneltems) { + expect(paneltems.length).toEqual(3); + expect(paneltems[0].key).toEqual('1'); + expect(paneltems[1].key).toEqual('2'); + expect(paneltems[2].key).toEqual('3'); + } else { + fail('Expect paneltems to be defined'); + } + }); + + test('clicking the button opens/closes the popover', () => { + const items = []; + const onClick = jest.fn(); + for (let i = 0; i < 3; i++) { + items.push( + {`item${i + 1}`} + ); + } + + const component = mountWithIntl(); + let popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(false); + const button = component.find(EuiButton); + button.simulate('click'); + popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(true); + button.simulate('click'); + popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(false); + }); +}); diff --git a/src/plugins/expressions/README.md b/src/plugins/expressions/README.md new file mode 100644 index 0000000000000..c1f032ace37c9 --- /dev/null +++ b/src/plugins/expressions/README.md @@ -0,0 +1,35 @@ +# `expressions` plugin + +This plugin provides methods which will parse & execute an *expression pipeline* +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + +Expression pipeline is a chain of functions that *pipe* its output to the +input of the next function. Functions can be configured using arguments provided +by the user. The final output of the expression pipeline can be rendered using +one of the *renderers* registered in `expressions` plugin. + +Expressions power visualizations in Dashboard and Lens, as well as, every +*element* in Canvas is backed by an expression. + +Below is an example of one Canvas element that fetches data using `essql` function, +pipes it further to `math` and `metric` functions, and final `render` function +renders the result. + +``` +filters +| essql + query="SELECT COUNT(timestamp) as total_errors + FROM kibana_sample_data_logs + WHERE tags LIKE '%warning%' OR tags LIKE '%error%'" +| math "total_errors" +| metric "TOTAL ISSUES" + metricFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=48 align="left" color="#FFFFFF" weight="normal" underline=false italic=false} + labelFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=30 align="left" color="#FFFFFF" weight="lighter" underline=false italic=false} +| render +``` + +![image](https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21-11ea-9e68-86f66862a183.png) + +[See Canvas documentation about expressions](https://www.elastic.co/guide/en/kibana/current/canvas-function-arguments.html). diff --git a/src/plugins/expressions/common/ast/format.test.ts b/src/plugins/expressions/common/ast/format.test.ts new file mode 100644 index 0000000000000..d680ab2e30ce4 --- /dev/null +++ b/src/plugins/expressions/common/ast/format.test.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 { formatExpression } from './format'; + +describe('formatExpression()', () => { + test('converts expression AST to string', () => { + const str = formatExpression({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + }, + function: 'foo', + }, + ], + }); + + expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + }); +}); diff --git a/src/plugins/expressions/common/ast/format.ts b/src/plugins/expressions/common/ast/format.ts new file mode 100644 index 0000000000000..985f07008b33d --- /dev/null +++ b/src/plugins/expressions/common/ast/format.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 { ExpressionAstExpression, ExpressionAstArgument } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { toExpression } = require('@kbn/interpreter/common'); + +export function format( + ast: ExpressionAstExpression | ExpressionAstArgument, + type: 'expression' | 'argument' +): string { + return toExpression(ast, type); +} + +export function formatExpression(ast: ExpressionAstExpression): string { + return format(ast, 'expression'); +} diff --git a/src/plugins/expressions/common/ast/index.ts b/src/plugins/expressions/common/ast/index.ts new file mode 100644 index 0000000000000..398718e8092b3 --- /dev/null +++ b/src/plugins/expressions/common/ast/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 './types'; +export * from './parse'; +export * from './parse_expression'; +export * from './format'; diff --git a/src/plugins/expressions/common/ast/parse.test.ts b/src/plugins/expressions/common/ast/parse.test.ts new file mode 100644 index 0000000000000..967091a52082f --- /dev/null +++ b/src/plugins/expressions/common/ast/parse.test.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 { parse } from './parse'; + +describe('parse()', () => { + test('parses an expression', () => { + const ast = parse('foo bar="baz"', 'expression'); + + expect(ast).toMatchObject({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + }, + function: 'foo', + }, + ], + }); + }); + + test('parses an argument', () => { + const arg = parse('foo', 'argument'); + expect(arg).toBe('foo'); + }); +}); diff --git a/src/plugins/expressions/common/ast/parse.ts b/src/plugins/expressions/common/ast/parse.ts new file mode 100644 index 0000000000000..0204694d1926d --- /dev/null +++ b/src/plugins/expressions/common/ast/parse.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 { ExpressionAstExpression, ExpressionAstArgument } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { parse: parseRaw } = require('@kbn/interpreter/common'); + +export function parse( + expression: string, + startRule: 'expression' | 'argument' +): ExpressionAstExpression | ExpressionAstArgument { + try { + return parseRaw(String(expression), { startRule }); + } catch (e) { + throw new Error(`Unable to parse expression: ${e.message}`); + } +} diff --git a/src/plugins/expressions/common/ast/parse_expression.test.ts b/src/plugins/expressions/common/ast/parse_expression.test.ts new file mode 100644 index 0000000000000..c387e58d9b787 --- /dev/null +++ b/src/plugins/expressions/common/ast/parse_expression.test.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. + */ + +import { parseExpression } from './parse_expression'; + +describe('parseExpression()', () => { + test('parses an expression', () => { + const ast = parseExpression('foo bar="baz"'); + + expect(ast).toMatchObject({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + }, + function: 'foo', + }, + ], + }); + }); + + test('parses an expression with sub-expression', () => { + const ast = parseExpression('foo bar="baz" quux={quix}'); + + expect(ast).toMatchObject({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + quux: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'quix', + arguments: {}, + }, + ], + }, + ], + }, + function: 'foo', + }, + ], + }); + }); +}); diff --git a/src/plugins/expressions/common/ast/parse_expression.ts b/src/plugins/expressions/common/ast/parse_expression.ts new file mode 100644 index 0000000000000..ae4d80bd1fb5b --- /dev/null +++ b/src/plugins/expressions/common/ast/parse_expression.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 { ExpressionAstExpression } from './types'; +import { parse } from './parse'; + +/** + * Given expression pipeline string, returns parsed AST. + * + * @param expression Expression pipeline string. + */ +export function parseExpression(expression: string): ExpressionAstExpression { + return parse(expression, 'expression') as ExpressionAstExpression; +} diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts new file mode 100644 index 0000000000000..82a7578dd4b89 --- /dev/null +++ b/src/plugins/expressions/common/ast/types.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 type ExpressionAstNode = + | ExpressionAstExpression + | ExpressionAstFunction + | ExpressionAstArgument; + +export interface ExpressionAstExpression { + type: 'expression'; + chain: ExpressionAstFunction[]; +} + +export interface ExpressionAstFunction { + type: 'function'; + function: string; + arguments: Record; +} + +export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression; diff --git a/src/plugins/expressions/common/execution/container.ts b/src/plugins/expressions/common/execution/container.ts new file mode 100644 index 0000000000000..d6271869134d1 --- /dev/null +++ b/src/plugins/expressions/common/execution/container.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 { + StateContainer, + createStateContainer, +} from '../../../kibana_utils/common/state_containers'; +import { ExecutorState, defaultState as executorDefaultState } from '../executor'; +import { ExpressionAstExpression } from '../ast'; +import { ExpressionValue } from '../expression_types'; + +export interface ExecutionState extends ExecutorState { + ast: ExpressionAstExpression; + + /** + * Tracks state of execution. + * + * - `not-started` - before .start() method was called. + * - `pending` - immediately after .start() method is called. + * - `result` - when expression execution completed. + * - `error` - when execution failed with error. + */ + state: 'not-started' | 'pending' | 'result' | 'error'; + + /** + * Result of the expression execution. + */ + result?: Output; + + /** + * Error happened during the execution. + */ + error?: Error; +} + +const executionDefaultState: ExecutionState = { + ...executorDefaultState, + state: 'not-started', + ast: { + type: 'expression', + chain: [], + }, +}; + +// eslint-disable-next-line +export interface ExecutionPureTransitions { + start: (state: ExecutionState) => () => ExecutionState; + setResult: (state: ExecutionState) => (result: Output) => ExecutionState; + setError: (state: ExecutionState) => (error: Error) => ExecutionState; +} + +export const executionPureTransitions: ExecutionPureTransitions = { + start: state => () => ({ + ...state, + state: 'pending', + }), + setResult: state => result => ({ + ...state, + state: 'result', + result, + }), + setError: state => error => ({ + ...state, + state: 'error', + error, + }), +}; + +export type ExecutionContainer = StateContainer< + ExecutionState, + ExecutionPureTransitions +>; + +const freeze = (state: T): T => state; + +export const createExecutionContainer = ( + state: ExecutionState = executionDefaultState +): ExecutionContainer => { + const container = createStateContainer< + ExecutionState, + ExecutionPureTransitions, + object + >( + state, + executionPureTransitions, + {}, + { + freeze, + } + ); + return container; +}; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts new file mode 100644 index 0000000000000..3937bd309327d --- /dev/null +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -0,0 +1,372 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Execution } from './execution'; +import { parseExpression } from '../ast'; +import { createUnitTestExecutor } from '../test_helpers'; +import { ExpressionFunctionDefinition } from '../../public'; + +const createExecution = ( + expression: string = 'foo bar=123', + context: Record = {} +) => { + const executor = createUnitTestExecutor(); + const execution = new Execution({ + executor, + ast: parseExpression(expression), + context, + }); + return execution; +}; + +const run = async ( + expression: string = 'foo bar=123', + context?: Record, + input: any = null +) => { + const execution = createExecution(expression, context); + execution.start(input); + return await execution.result; +}; + +describe('Execution', () => { + test('can instantiate', () => { + const execution = createExecution('foo bar=123'); + expect(execution.params.ast.chain[0].arguments.bar).toEqual([123]); + }); + + test('initial input is null at creation', () => { + const execution = createExecution(); + expect(execution.input).toBe(null); + }); + + test('creates default ExecutionContext', () => { + const execution = createExecution(); + expect(execution.context).toMatchObject({ + getInitialInput: expect.any(Function), + variables: expect.any(Object), + types: expect.any(Object), + }); + }); + + test('executes a single clog function in expression pipeline', async () => { + const execution = createExecution('clog'); + /* eslint-disable no-console */ + const console$log = console.log; + const spy = (console.log = jest.fn()); + /* eslint-enable no-console */ + + execution.start(123); + const result = await execution.result; + + expect(result).toBe(123); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(123); + + /* eslint-disable no-console */ + console.log = console$log; + /* eslint-enable no-console */ + }); + + test('executes a chain of multiple "add" functions', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3'); + execution.start({ + type: 'num', + value: -1, + }); + + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 5, + }); + }); + + test('executes a chain of "add" and "mult" functions', async () => { + const execution = createExecution('add val=5 | mult val=-1 | add val=-10 | mult val=2'); + execution.start({ + type: 'num', + value: 0, + }); + + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: -30, + }); + }); + + test('casts input to correct type', async () => { + const execution = createExecution('add val=1'); + + // Below 1 is cast to { type: 'num', value: 1 }. + execution.start(1); + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 2, + }); + }); + + describe('execution context', () => { + test('context.variables is an object', async () => { + const { result } = (await run('introspectContext key="variables"')) as any; + expect(typeof result).toBe('object'); + }); + + test('context.types is an object', async () => { + const { result } = (await run('introspectContext key="types"')) as any; + expect(typeof result).toBe('object'); + }); + + test('context.abortSignal is an object', async () => { + const { result } = (await run('introspectContext key="abortSignal"')) as any; + expect(typeof result).toBe('object'); + }); + + test('context.inspectorAdapters is an object', async () => { + const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; + expect(typeof result).toBe('object'); + }); + + test('unknown context key is undefined', async () => { + const { result } = (await run('introspectContext key="foo"')) as any; + expect(typeof result).toBe('undefined'); + }); + + test('can set context variables', async () => { + const variables = { foo: 'bar' }; + const result = await run('var name="foo"', { variables }); + expect(result).toBe('bar'); + }); + }); + + describe('inspector adapters', () => { + test('by default, "data" and "requests" inspector adapters are available', async () => { + const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; + expect(result).toMatchObject({ + data: expect.any(Object), + requests: expect.any(Object), + }); + }); + + test('can set custom inspector adapters', async () => { + const inspectorAdapters = {}; + const { result } = (await run('introspectContext key="inspectorAdapters"', { + inspectorAdapters, + })) as any; + expect(result).toBe(inspectorAdapters); + }); + + test('can access custom inspector adapters on Execution object', async () => { + const inspectorAdapters = {}; + const execution = createExecution('introspectContext key="inspectorAdapters"', { + inspectorAdapters, + }); + expect(execution.inspectorAdapters).toBe(inspectorAdapters); + }); + }); + + describe('expression abortion', () => { + test('context has abortSignal object', async () => { + const { result } = (await run('introspectContext key="abortSignal"')) as any; + + expect(typeof result).toBe('object'); + expect((result as AbortSignal).aborted).toBe(false); + }); + }); + + describe('expression execution', () => { + test('supports default argument alias _', async () => { + const execution = createExecution('add val=1 | add 2'); + execution.start({ + type: 'num', + value: 0, + }); + + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 3, + }); + }); + + test('can execute async functions', async () => { + const res = await run('sleep 10 | sleep 10'); + expect(res).toBe(null); + }); + + test('result is undefined until execution completes', async () => { + const execution = createExecution('sleep 10'); + expect(execution.state.get().result).toBe(undefined); + execution.start(null); + expect(execution.state.get().result).toBe(undefined); + await new Promise(r => setTimeout(r, 1)); + expect(execution.state.get().result).toBe(undefined); + await new Promise(r => setTimeout(r, 11)); + expect(execution.state.get().result).toBe(null); + }); + }); + + describe('when function throws', () => { + test('error is reported in output object', async () => { + const result = await run('error "foobar"'); + + expect(result).toMatchObject({ + type: 'error', + }); + }); + + test('error message is prefixed with function name', async () => { + const result = await run('error "foobar"'); + + expect(result).toMatchObject({ + error: { + message: `[error] > foobar`, + }, + }); + }); + + test('returns error of the first function that throws', async () => { + const result = await run('error "foo" | error "bar"'); + + expect(result).toMatchObject({ + error: { + message: `[error] > foo`, + }, + }); + }); + + test('when function throws, execution still succeeds', async () => { + const execution = await createExecution('error "foo"'); + execution.start(null); + + const result = await execution.result; + + expect(result).toMatchObject({ + type: 'error', + }); + expect(execution.state.get().state).toBe('result'); + expect(execution.state.get().result).toMatchObject({ + type: 'error', + }); + }); + + test('does not execute remaining functions in pipeline', async () => { + const spy: ExpressionFunctionDefinition<'spy', any, {}, any> = { + name: 'spy', + args: {}, + help: '', + fn: jest.fn(), + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(spy); + + await executor.run('error "..." | spy', null); + + expect(spy.fn).toHaveBeenCalledTimes(0); + }); + }); + + describe('state', () => { + test('execution state is "not-started" before .start() is called', async () => { + const execution = createExecution('var foo'); + expect(execution.state.get().state).toBe('not-started'); + }); + + test('execution state is "pending" after .start() was called', async () => { + const execution = createExecution('var foo'); + execution.start(null); + expect(execution.state.get().state).toBe('pending'); + }); + + test('execution state is "pending" while execution is in progress', async () => { + const execution = createExecution('sleep 20'); + execution.start(null); + await new Promise(r => setTimeout(r, 5)); + expect(execution.state.get().state).toBe('pending'); + }); + + test('execution state is "result" when execution successfully completes', async () => { + const execution = createExecution('sleep 1'); + execution.start(null); + await new Promise(r => setTimeout(r, 30)); + expect(execution.state.get().state).toBe('result'); + }); + + test('execution state is "result" when execution successfully completes - 2', async () => { + const execution = createExecution('var foo'); + execution.start(null); + await execution.result; + expect(execution.state.get().state).toBe('result'); + }); + }); + + describe('sub-expressions', () => { + test('executes sub-expressions', async () => { + const result = await run('add val={add 5 | access "value"}', {}, null); + + expect(result).toMatchObject({ + type: 'num', + value: 5, + }); + }); + }); + + describe('when arguments are missing', () => { + test('when required argument is missing and has not alias, returns error', async () => { + const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = { + name: 'requiredArg', + args: { + arg: { + help: '', + required: true, + }, + }, + help: '', + fn: jest.fn(), + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(requiredArg); + const result = await executor.run('requiredArg', null, {}); + + expect(result).toMatchObject({ + type: 'error', + error: { + message: '[requiredArg] > requiredArg requires an argument', + }, + }); + }); + + test('when required argument is missing and has alias, returns error', async () => { + const result = await run('var_set', {}); + + expect(result).toMatchObject({ + type: 'error', + error: { + message: '[var_set] > var_set requires an "name" argument', + }, + }); + }); + }); +}); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts new file mode 100644 index 0000000000000..7f4efafc13de8 --- /dev/null +++ b/src/plugins/expressions/common/execution/execution.ts @@ -0,0 +1,330 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { keys, last, mapValues, reduce, zipObject } from 'lodash'; +import { Executor } from '../executor'; +import { createExecutionContainer, ExecutionContainer } from './container'; +import { createError } from '../util'; +import { Defer } from '../../../kibana_utils/common'; +import { RequestAdapter, DataAdapter } from '../../../inspector/common'; +import { isExpressionValueError } from '../expression_types/specs/error'; +import { ExpressionAstExpression, ExpressionAstFunction, parse } from '../ast'; +import { ExecutionContext, DefaultInspectorAdapters } from './types'; +import { getType } from '../expression_types'; +import { ArgumentType, ExpressionFunction } from '../expression_functions'; +import { getByAlias } from '../util/get_by_alias'; + +export interface ExecutionParams< + ExtraContext extends Record = Record +> { + executor: Executor; + ast: ExpressionAstExpression; + context?: ExtraContext; +} + +const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ + requests: new RequestAdapter(), + data: new DataAdapter(), +}); + +export class Execution< + ExtraContext extends Record = Record, + Input = unknown, + Output = unknown, + InspectorAdapters = ExtraContext['inspectorAdapters'] extends object + ? ExtraContext['inspectorAdapters'] + : DefaultInspectorAdapters +> { + /** + * Dynamic state of the execution. + */ + public readonly state: ExecutionContainer; + + /** + * Initial input of the execution. + * + * N.B. It is initialized to `null` rather than `undefined` for legacy reasons, + * because in legacy interpreter it was set to `null` by default. + */ + public input: Input = null as any; + + /** + * Execution context - object that allows to do side-effects. Context is passed + * to every function. + */ + public readonly context: ExecutionContext & ExtraContext; + + /** + * AbortController to cancel this Execution. + */ + private readonly abortController = new AbortController(); + + /** + * Whether .start() method has been called. + */ + private hasStarted: boolean = false; + + /** + * Future that tracks result or error of this execution. + */ + private readonly firstResultFuture = new Defer(); + + public get result(): Promise { + return this.firstResultFuture.promise; + } + + public get inspectorAdapters(): InspectorAdapters { + return this.context.inspectorAdapters; + } + + constructor(public readonly params: ExecutionParams) { + const { executor, ast } = params; + this.state = createExecutionContainer({ + ...executor.state.get(), + state: 'not-started', + ast, + }); + + this.context = { + getInitialInput: () => this.input, + variables: {}, + types: executor.getTypes(), + abortSignal: this.abortController.signal, + ...(params.context || ({} as ExtraContext)), + inspectorAdapters: (params.context && params.context.inspectorAdapters + ? params.context.inspectorAdapters + : createDefaultInspectorAdapters()) as InspectorAdapters, + }; + } + + /** + * Stop execution of expression. + */ + cancel() { + this.abortController.abort(); + } + + /** + * Call this method to start execution. + * + * N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons, + * because in legacy interpreter it was set to `null` by default. + */ + public start(input: Input = null as any) { + if (this.hasStarted) throw new Error('Execution already started.'); + this.hasStarted = true; + + this.input = input; + this.state.transitions.start(); + + const { resolve, reject } = this.firstResultFuture; + this.invokeChain(this.state.get().ast.chain, input).then(resolve, reject); + + this.firstResultFuture.promise.then( + result => { + this.state.transitions.setResult(result); + }, + error => { + this.state.transitions.setError(error); + } + ); + } + + async invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise { + if (!chainArr.length) return input; + + for (const link of chainArr) { + // if execution was aborted return error + if (this.context.abortSignal && this.context.abortSignal.aborted) { + return createError({ + message: 'The expression was aborted.', + name: 'AbortError', + }); + } + + const { function: fnName, arguments: fnArgs } = link; + const fnDef = getByAlias(this.state.get().functions, fnName); + + if (!fnDef) { + return createError({ message: `Function ${fnName} could not be found.` }); + } + + try { + // Resolve arguments before passing to function + // resolveArgs returns an object because the arguments themselves might + // actually have a 'then' function which would be treated as a promise + const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs); + const output = await this.invokeFunction(fnDef, input, resolvedArgs); + if (getType(output) === 'error') return output; + input = output; + } catch (e) { + e.message = `[${fnName}] > ${e.message}`; + return createError(e); + } + } + + return input; + } + + async invokeFunction( + fn: ExpressionFunction, + input: unknown, + args: Record + ): Promise { + const normalizedInput = this.cast(input, fn.inputTypes); + const output = await fn.fn(normalizedInput, args, this.context); + + // Validate that the function returned the type it said it would. + // This isn't required, but it keeps function developers honest. + const returnType = getType(output); + const expectedType = fn.type; + if (expectedType && returnType !== expectedType) { + throw new Error( + `Function '${fn.name}' should return '${expectedType}',` + + ` actually returned '${returnType}'` + ); + } + + // Validate the function output against the type definition's validate function. + const type = this.context.types[fn.type]; + if (type && type.validate) { + try { + type.validate(output); + } catch (e) { + throw new Error(`Output of '${fn.name}' is not a valid type '${fn.type}': ${e}`); + } + } + + return output; + } + + public cast(value: any, toTypeNames?: string[]) { + // If you don't give us anything to cast to, you'll get your input back + if (!toTypeNames || toTypeNames.length === 0) return value; + + // No need to cast if node is already one of the valid types + const fromTypeName = getType(value); + if (toTypeNames.includes(fromTypeName)) return value; + + const { types } = this.state.get(); + const fromTypeDef = types[fromTypeName]; + + for (const toTypeName of toTypeNames) { + // First check if the current type can cast to this type + if (fromTypeDef && fromTypeDef.castsTo(toTypeName)) { + return fromTypeDef.to(value, toTypeName, types); + } + + // If that isn't possible, check if this type can cast from the current type + const toTypeDef = types[toTypeName]; + if (toTypeDef && toTypeDef.castsFrom(fromTypeName)) return toTypeDef.from(value, types); + } + + throw new Error(`Can not cast '${fromTypeName}' to any of '${toTypeNames.join(', ')}'`); + } + + // Processes the multi-valued AST argument values into arguments that can be passed to the function + async resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise { + const argDefs = fnDef.args; + + // Use the non-alias name from the argument definition + const dealiasedArgAsts = reduce( + argAsts, + (acc, argAst, argName) => { + const argDef = getByAlias(argDefs, argName); + if (!argDef) { + throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`); + } + acc[argDef.name] = (acc[argDef.name] || []).concat(argAst); + return acc; + }, + {} as any + ); + + // Check for missing required arguments. + for (const argDef of Object.values(argDefs)) { + const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType< + any + > & { name: string }; + if ( + typeof argDefault !== 'undefined' || + !required || + typeof dealiasedArgAsts[argName] !== 'undefined' + ) + continue; + + if (!aliases || aliases.length === 0) { + throw new Error(`${fnDef.name} requires an argument`); + } + + // use an alias if _ is the missing arg + const errorArg = argName === '_' ? aliases[0] : argName; + throw new Error(`${fnDef.name} requires an "${errorArg}" argument`); + } + + // Fill in default values from argument definition + const argAstsWithDefaults = reduce( + argDefs, + (acc: any, argDef: any, argName: any) => { + if (typeof acc[argName] === 'undefined' && typeof argDef.default !== 'undefined') { + acc[argName] = [parse(argDef.default, 'argument')]; + } + + return acc; + }, + dealiasedArgAsts + ); + + // Create the functions to resolve the argument ASTs into values + // These are what are passed to the actual functions if you opt out of resolving + const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { + return asts.map((item: ExpressionAstExpression) => { + return async (subInput = input) => { + const output = await this.params.executor.interpret(item, subInput); + if (isExpressionValueError(output)) throw output.error; + const casted = this.cast(output, argDefs[argName as any].types); + return casted; + }; + }); + }); + + const argNames = keys(resolveArgFns); + + // Actually resolve unless the argument definition says not to + const resolvedArgValues = await Promise.all( + argNames.map(argName => { + const interpretFns = resolveArgFns[argName]; + if (!argDefs[argName].resolve) return interpretFns; + return Promise.all(interpretFns.map((fn: any) => fn())); + }) + ); + + const resolvedMultiArgs = zipObject(argNames, resolvedArgValues); + + // Just return the last unless the argument definition allows multiple + const resolvedArgs = mapValues(resolvedMultiArgs, (argValues, argName) => { + if (argDefs[argName as any].multi) return argValues; + return last(argValues as any); + }); + + // Return an object here because the arguments themselves might actually have a 'then' + // function which would be treated as a promise + return { resolvedArgs }; + } +} diff --git a/src/plugins/expressions/public/registries/index.ts b/src/plugins/expressions/common/execution/index.ts similarity index 88% rename from src/plugins/expressions/public/registries/index.ts rename to src/plugins/expressions/common/execution/index.ts index 16c8d8fc4c93a..2452b0999d23e 100644 --- a/src/plugins/expressions/public/registries/index.ts +++ b/src/plugins/expressions/common/execution/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export * from './type_registry'; -export * from './function_registry'; -export * from './render_registry'; +export * from './types'; +export * from './container'; +export * from './execution'; diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts new file mode 100644 index 0000000000000..e05eb7cc94486 --- /dev/null +++ b/src/plugins/expressions/common/execution/types.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ExpressionType } from '../expression_types'; +import { DataAdapter, RequestAdapter } from '../../../inspector/common'; +import { TimeRange, Query, esFilters } from '../../../data/common'; + +/** + * `ExecutionContext` is an object available to all functions during a single execution; + * it provides various methods to perform side-effects. + */ +export interface ExecutionContext { + /** + * Get initial input with which execution started. + */ + getInitialInput: () => Input; + + /** + * Context variables that can be consumed using `var` and `var_set` functions. + */ + variables: Record; + + /** + * A map of available expression types. + */ + types: Record; + + /** + * Adds ability to abort current execution. + */ + abortSignal: AbortSignal; + + /** + * Adapters for `inspector` plugin. + */ + inspectorAdapters: InspectorAdapters; + + /** + * Search context in which expression should operate. + */ + search?: ExecutionContextSearch; +} + +/** + * Default inspector adapters created if inspector adapters are not set explicitly. + */ +export interface DefaultInspectorAdapters { + requests: RequestAdapter; + data: DataAdapter; +} + +export interface ExecutionContextSearch { + filters?: esFilters.Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; +} diff --git a/src/plugins/expressions/common/executor/container.ts b/src/plugins/expressions/common/executor/container.ts new file mode 100644 index 0000000000000..c9c1ab34e7ac3 --- /dev/null +++ b/src/plugins/expressions/common/executor/container.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 { + StateContainer, + createStateContainer, +} from '../../../kibana_utils/common/state_containers'; +import { ExpressionFunction } from '../expression_functions'; +import { ExpressionType } from '../expression_types'; + +export interface ExecutorState = Record> { + functions: Record; + types: Record; + context: Context; +} + +export const defaultState: ExecutorState = { + functions: {}, + types: {}, + context: {}, +}; + +export interface ExecutorPureTransitions { + addFunction: (state: ExecutorState) => (fn: ExpressionFunction) => ExecutorState; + addType: (state: ExecutorState) => (type: ExpressionType) => ExecutorState; + extendContext: (state: ExecutorState) => (extraContext: Record) => ExecutorState; +} + +export const pureTransitions: ExecutorPureTransitions = { + addFunction: state => fn => ({ ...state, functions: { ...state.functions, [fn.name]: fn } }), + addType: state => type => ({ ...state, types: { ...state.types, [type.name]: type } }), + extendContext: state => extraContext => ({ + ...state, + context: { ...state.context, ...extraContext }, + }), +}; + +export interface ExecutorPureSelectors { + getFunction: (state: ExecutorState) => (id: string) => ExpressionFunction | null; + getType: (state: ExecutorState) => (id: string) => ExpressionType | null; + getContext: (state: ExecutorState) => () => ExecutorState['context']; +} + +export const pureSelectors: ExecutorPureSelectors = { + getFunction: state => id => state.functions[id] || null, + getType: state => id => state.types[id] || null, + getContext: ({ context }) => () => context, +}; + +export type ExecutorContainer< + Context extends Record = Record +> = StateContainer, ExecutorPureTransitions, ExecutorPureSelectors>; + +export const createExecutorContainer = < + Context extends Record = Record +>( + state: ExecutorState = defaultState +): ExecutorContainer => { + const container = createStateContainer< + ExecutorState, + ExecutorPureTransitions, + ExecutorPureSelectors + >(state, pureTransitions, pureSelectors); + return container; +}; diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts new file mode 100644 index 0000000000000..502728bb66403 --- /dev/null +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Executor } from './executor'; +import * as expressionTypes from '../expression_types'; +import * as expressionFunctions from '../expression_functions'; +import { Execution } from '../execution'; +import { parseExpression } from '../ast'; + +describe('Executor', () => { + test('can instantiate', () => { + new Executor(); + }); + + describe('type registry', () => { + test('can register a type', () => { + const executor = new Executor(); + executor.registerType(expressionTypes.datatable); + }); + + test('can register all types', () => { + const executor = new Executor(); + for (const type of expressionTypes.typeSpecs) executor.registerType(type); + }); + + test('can retrieve all types', () => { + const executor = new Executor(); + executor.registerType(expressionTypes.datatable); + const types = executor.getTypes(); + expect(Object.keys(types)).toEqual(['datatable']); + }); + + test('can retrieve all types - 2', () => { + const executor = new Executor(); + for (const type of expressionTypes.typeSpecs) executor.registerType(type); + const types = executor.getTypes(); + expect(Object.keys(types).sort()).toEqual( + expressionTypes.typeSpecs.map(spec => spec.name).sort() + ); + }); + }); + + describe('function registry', () => { + test('can register a function', () => { + const executor = new Executor(); + executor.registerFunction(expressionFunctions.clog); + }); + + test('can register all functions', () => { + const executor = new Executor(); + for (const functionDefinition of expressionFunctions.functionSpecs) + executor.registerFunction(functionDefinition); + }); + + test('can retrieve all functions', () => { + const executor = new Executor(); + executor.registerFunction(expressionFunctions.clog); + const functions = executor.getFunctions(); + expect(Object.keys(functions)).toEqual(['clog']); + }); + + test('can retrieve all functions - 2', () => { + const executor = new Executor(); + for (const functionDefinition of expressionFunctions.functionSpecs) + executor.registerFunction(functionDefinition); + const functions = executor.getFunctions(); + expect(Object.keys(functions).sort()).toEqual( + expressionFunctions.functionSpecs.map(spec => spec.name).sort() + ); + }); + }); + + describe('context', () => { + test('context is empty by default', () => { + const executor = new Executor(); + expect(executor.context).toEqual({}); + }); + + test('can extend context', () => { + const executor = new Executor(); + executor.extendContext({ + foo: 'bar', + }); + expect(executor.context).toEqual({ + foo: 'bar', + }); + }); + + test('can extend context multiple times with multiple keys', () => { + const executor = new Executor(); + const abortSignal = {}; + const env = {}; + + executor.extendContext({ + foo: 'bar', + }); + executor.extendContext({ + abortSignal, + env, + }); + + expect(executor.context).toEqual({ + foo: 'bar', + abortSignal, + env, + }); + }); + }); + + describe('execution', () => { + describe('createExecution()', () => { + test('returns Execution object from string', () => { + const executor = new Executor(); + const execution = executor.createExecution('foo bar="baz"'); + + expect(execution).toBeInstanceOf(Execution); + expect(execution.params.ast.chain[0].function).toBe('foo'); + }); + + test('returns Execution object from AST', () => { + const executor = new Executor(); + const ast = parseExpression('foo bar="baz"'); + const execution = executor.createExecution(ast); + + expect(execution).toBeInstanceOf(Execution); + expect(execution.params.ast.chain[0].function).toBe('foo'); + }); + + test('Execution inherits context from Executor', () => { + const executor = new Executor(); + const foo = {}; + executor.extendContext({ foo }); + const execution = executor.createExecution('foo bar="baz"'); + + expect((execution.context as any).foo).toBe(foo); + }); + }); + }); +}); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts new file mode 100644 index 0000000000000..5c27201b43fc0 --- /dev/null +++ b/src/plugins/expressions/common/executor/executor.ts @@ -0,0 +1,204 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 max-classes-per-file */ + +import { ExecutorState, ExecutorContainer } from './container'; +import { createExecutorContainer } from './container'; +import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; +import { Execution } from '../execution/execution'; +import { IRegistry } from '../types'; +import { ExpressionType } from '../expression_types/expression_type'; +import { AnyExpressionTypeDefinition } from '../expression_types/types'; +import { getType } from '../expression_types'; +import { ExpressionAstExpression, ExpressionAstNode, parseExpression } from '../ast'; +import { typeSpecs } from '../expression_types/specs'; +import { functionSpecs } from '../expression_functions/specs'; + +export class TypesRegistry implements IRegistry { + constructor(private readonly executor: Executor) {} + + public register( + typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) + ) { + this.executor.registerType(typeDefinition); + } + + public get(id: string): ExpressionType | null { + return this.executor.state.selectors.getType(id); + } + + public toJS(): Record { + return this.executor.getTypes(); + } + + public toArray(): ExpressionType[] { + return Object.values(this.toJS()); + } +} + +export class FunctionsRegistry implements IRegistry { + constructor(private readonly executor: Executor) {} + + public register( + functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) + ) { + this.executor.registerFunction(functionDefinition); + } + + public get(id: string): ExpressionFunction | null { + return this.executor.state.selectors.getFunction(id); + } + + public toJS(): Record { + return this.executor.getFunctions(); + } + + public toArray(): ExpressionFunction[] { + return Object.values(this.toJS()); + } +} + +export class Executor = Record> { + static createWithDefaults = Record>( + state?: ExecutorState + ): Executor { + const executor = new Executor(state); + for (const type of typeSpecs) executor.registerType(type); + for (const func of functionSpecs) executor.registerFunction(func); + return executor; + } + + public readonly state: ExecutorContainer; + + /** + * @deprecated + */ + public readonly functions: FunctionsRegistry; + + /** + * @deprecated + */ + public readonly types: TypesRegistry; + + constructor(state?: ExecutorState) { + this.state = createExecutorContainer(state); + this.functions = new FunctionsRegistry(this); + this.types = new TypesRegistry(this); + } + + public registerFunction( + functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) + ) { + const fn = new ExpressionFunction( + typeof functionDefinition === 'object' ? functionDefinition : functionDefinition() + ); + this.state.transitions.addFunction(fn); + } + + public getFunction(name: string): ExpressionFunction | undefined { + return this.state.get().functions[name]; + } + + public getFunctions(): Record { + return { ...this.state.get().functions }; + } + + public registerType( + typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) + ) { + const type = new ExpressionType( + typeof typeDefinition === 'object' ? typeDefinition : typeDefinition() + ); + this.state.transitions.addType(type); + } + + public getType(name: string): ExpressionType | undefined { + return this.state.get().types[name]; + } + + public getTypes(): Record { + return { ...this.state.get().types }; + } + + public extendContext(extraContext: Record) { + this.state.transitions.extendContext(extraContext); + } + + public get context(): Record { + return this.state.selectors.getContext(); + } + + public async interpret(ast: ExpressionAstNode, input: T): Promise { + switch (getType(ast)) { + case 'expression': + return await this.interpretExpression(ast as ExpressionAstExpression, input); + case 'string': + case 'number': + case 'null': + case 'boolean': + return ast; + default: + throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`); + } + } + + public async interpretExpression( + ast: string | ExpressionAstExpression, + input: T + ): Promise { + const execution = this.createExecution(ast); + execution.start(input); + return await execution.result; + } + + /** + * Execute expression and return result. + * + * @param ast Expression AST or a string representing expression. + * @param input Initial input to the first expression function. + * @param context Extra global context object that will be merged into the + * expression global context object that is provided to each function to allow side-effects. + */ + public async run< + Input, + Output, + ExtraContext extends Record = Record + >(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) { + const execution = this.createExecution(ast, context); + execution.start(input); + return (await execution.result) as Output; + } + + public createExecution = Record>( + ast: string | ExpressionAstExpression, + context: ExtraContext = {} as ExtraContext + ): Execution { + if (typeof ast === 'string') ast = parseExpression(ast); + const execution = new Execution({ + ast, + executor: this, + context: { + ...this.context, + ...context, + } as Context & ExtraContext, + }); + return execution; + } +} diff --git a/src/plugins/expressions/common/executor/index.ts b/src/plugins/expressions/common/executor/index.ts new file mode 100644 index 0000000000000..ea49dfc85c1f5 --- /dev/null +++ b/src/plugins/expressions/common/executor/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 * from './container'; +export * from './executor'; diff --git a/src/plugins/expressions/common/types/arguments.ts b/src/plugins/expressions/common/expression_functions/arguments.ts similarity index 99% rename from src/plugins/expressions/common/types/arguments.ts rename to src/plugins/expressions/common/expression_functions/arguments.ts index 20bec9359a593..38cee64aca521 100644 --- a/src/plugins/expressions/common/types/arguments.ts +++ b/src/plugins/expressions/common/expression_functions/arguments.ts @@ -17,7 +17,7 @@ * under the License. */ -import { KnownTypeToString, TypeString, UnmappedTypeStrings } from './common'; +import { KnownTypeToString, TypeString, UnmappedTypeStrings } from '../types/common'; /** * This type represents all of the possible combinations of properties of an diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts new file mode 100644 index 0000000000000..71f0d91510136 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { AnyExpressionFunctionDefinition } from './types'; +import { ExpressionFunctionParameter } from './expression_function_parameter'; +import { ExpressionValue } from '../expression_types/types'; +import { ExecutionContext } from '../execution'; + +export class ExpressionFunction { + /** + * Name of function + */ + name: string; + + /** + * Aliases that can be used instead of `name`. + */ + aliases: string[]; + + /** + * Return type of function. This SHOULD be supplied. We use it for UI + * and autocomplete hinting. We may also use it for optimizations in + * the future. + */ + type: string; + + /** + * Function to run function (context, args) + */ + fn: (input: ExpressionValue, params: Record, handlers: object) => ExpressionValue; + + /** + * A short help text. + */ + help: string; + + /** + * Specification of expression function parameters. + */ + args: Record = {}; + + /** + * Type of inputs that this function supports. + */ + inputTypes: string[] | undefined; + + constructor(functionDefinition: AnyExpressionFunctionDefinition) { + const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition; + + this.name = name; + this.type = type; + this.aliases = aliases || []; + this.fn = (input, params, handlers) => + Promise.resolve(fn(input, params, handlers as ExecutionContext)); + this.help = help || ''; + this.inputTypes = inputTypes || context?.types; + + for (const [key, arg] of Object.entries(args || {})) { + this.args[key] = new ExpressionFunctionParameter(key, arg); + } + } + + accepts = (type: string): boolean => { + // If you don't tell us input types, we'll assume you don't care what you get. + if (!this.inputTypes) return true; + return this.inputTypes.indexOf(type) > -1; + }; +} diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts new file mode 100644 index 0000000000000..e94c0fa8a5b50 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/expression_function_parameter.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 { ArgumentType } from './arguments'; + +export class ExpressionFunctionParameter { + name: string; + required: boolean; + help: string; + types: string[]; + default: any; + aliases: string[]; + multi: boolean; + resolve: boolean; + options: any[]; + + constructor(name: string, arg: ArgumentType) { + const { required, help, types, aliases, multi, resolve, options } = arg; + + if (name === '_') { + throw Error('Arg names must not be _. Use it in aliases instead.'); + } + + this.name = name; + this.required = !!required; + this.help = help || ''; + this.types = types || []; + this.default = arg.default; + this.aliases = aliases || []; + this.multi = !!multi; + this.resolve = resolve == null ? true : resolve; + this.options = options || []; + } + + accepts(type: string) { + if (!this.types.length) return true; + return this.types.indexOf(type) > -1; + } +} diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts new file mode 100644 index 0000000000000..e52f7ec090282 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/expression_function_parameters.test.ts @@ -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 { ExpressionFunctionParameter } from './expression_function_parameter'; + +describe('ExpressionFunctionParameter', () => { + test('can instantiate', () => { + const param = new ExpressionFunctionParameter('foo', { + help: 'bar', + }); + + expect(param.name).toBe('foo'); + }); + + test('checks supported types', () => { + const param = new ExpressionFunctionParameter('foo', { + help: 'bar', + types: ['baz', 'quux'], + }); + + expect(param.accepts('baz')).toBe(true); + expect(param.accepts('quux')).toBe(true); + expect(param.accepts('quix')).toBe(false); + }); + + test('if no types are provided, then accepts any type', () => { + const param = new ExpressionFunctionParameter('foo', { + help: 'bar', + }); + + expect(param.accepts('baz')).toBe(true); + expect(param.accepts('quux')).toBe(true); + expect(param.accepts('quix')).toBe(true); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/index.ts b/src/plugins/expressions/common/expression_functions/index.ts new file mode 100644 index 0000000000000..b29e6b78b8f4d --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/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 './types'; +export * from './arguments'; +export * from './expression_function_parameter'; +export * from './expression_function'; +export * from './specs'; diff --git a/src/plugins/expressions/public/functions/clog.ts b/src/plugins/expressions/common/expression_functions/specs/clog.ts similarity index 71% rename from src/plugins/expressions/public/functions/clog.ts rename to src/plugins/expressions/common/expression_functions/specs/clog.ts index 2931b3b00d345..7839f1fc7998d 100644 --- a/src/plugins/expressions/public/functions/clog.ts +++ b/src/plugins/expressions/common/expression_functions/specs/clog.ts @@ -17,19 +17,15 @@ * under the License. */ -import { ExpressionFunction } from '../../common/types'; +import { ExpressionFunctionDefinition } from '../types'; -const name = 'clog'; - -type Context = any; -type ClogExpressionFunction = ExpressionFunction; - -export const clog = (): ClogExpressionFunction => ({ - name, +export const clog: ExpressionFunctionDefinition<'clog', unknown, {}, unknown> = { + name: 'clog', args: {}, help: 'Outputs the context to the console', - fn: context => { - console.log(context); // eslint-disable-line no-console - return context; + fn: (input: unknown) => { + // eslint-disable-next-line no-console + console.log(input); + return input; }, -}); +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/font.ts b/src/plugins/expressions/common/expression_functions/specs/font.ts new file mode 100644 index 0000000000000..3e305998a0157 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/font.ts @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ExpressionFunctionDefinition } from '../types'; +import { openSans, FontLabel as FontFamily } from '../../fonts'; +import { CSSStyle, FontStyle, FontWeight, Style, TextAlignment, TextDecoration } from '../../types'; + +const dashify = (str: string) => { + return str + .trim() + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/\W/g, m => (/[À-ž]/.test(m) ? m : '-')) + .replace(/^-+|-+$/g, '') + .toLowerCase(); +}; + +const inlineStyle = (obj: Record) => { + if (!obj) return ''; + const styles = Object.keys(obj).map(key => { + const prop = dashify(key); + const line = prop.concat(':').concat(String(obj[key])); + return line; + }); + return styles.join(';'); +}; + +interface Arguments { + align?: TextAlignment; + color?: string; + family?: FontFamily; + italic?: boolean; + lHeight?: number | null; + size?: number; + underline?: boolean; + weight?: FontWeight; +} + +export const font: ExpressionFunctionDefinition<'font', null, Arguments, Style> = { + name: 'font', + aliases: [], + type: 'style', + help: i18n.translate('expressions.functions.fontHelpText', { + defaultMessage: 'Create a font style.', + }), + inputTypes: ['null'], + args: { + align: { + default: 'left', + help: i18n.translate('expressions.functions.font.args.alignHelpText', { + defaultMessage: 'The horizontal text alignment.', + }), + options: Object.values(TextAlignment), + types: ['string'], + }, + color: { + help: i18n.translate('expressions.functions.font.args.colorHelpText', { + defaultMessage: 'The text color.', + }), + types: ['string'], + }, + family: { + default: `"${openSans.value}"`, + help: i18n.translate('expressions.functions.font.args.familyHelpText', { + defaultMessage: 'An acceptable {css} web font string', + values: { + css: 'CSS', + }, + }), + types: ['string'], + }, + italic: { + default: false, + help: i18n.translate('expressions.functions.font.args.italicHelpText', { + defaultMessage: 'Italicize the text?', + }), + options: [true, false], + types: ['boolean'], + }, + lHeight: { + default: null, + aliases: ['lineHeight'], + help: i18n.translate('expressions.functions.font.args.lHeightHelpText', { + defaultMessage: 'The line height in pixels', + }), + types: ['number', 'null'], + }, + size: { + default: 14, + help: i18n.translate('expressions.functions.font.args.sizeHelpText', { + defaultMessage: 'The font size in pixels', + }), + types: ['number'], + }, + underline: { + default: false, + help: i18n.translate('expressions.functions.font.args.underlineHelpText', { + defaultMessage: 'Underline the text?', + }), + options: [true, false], + types: ['boolean'], + }, + weight: { + default: 'normal', + help: i18n.translate('expressions.functions.font.args.weightHelpText', { + defaultMessage: 'The font weight. For example, {list}, or {end}.', + values: { + list: Object.values(FontWeight) + .slice(0, -1) + .map(weight => `\`"${weight}"\``) + .join(', '), + end: `\`"${Object.values(FontWeight).slice(-1)[0]}"\``, + }, + }), + options: Object.values(FontWeight), + types: ['string'], + }, + }, + fn: (input, args) => { + if (!Object.values(FontWeight).includes(args.weight!)) { + throw new Error( + i18n.translate('expressions.functions.font.invalidFontWeightErrorMessage', { + defaultMessage: "Invalid font weight: '{weight}'", + values: { + weight: args.weight, + }, + }) + ); + } + if (!Object.values(TextAlignment).includes(args.align!)) { + throw new Error( + i18n.translate('expressions.functions.font.invalidTextAlignmentErrorMessage', { + defaultMessage: "Invalid text alignment: '{align}'", + values: { + align: args.align, + }, + }) + ); + } + + // the line height shouldn't ever be lower than the size, and apply as a + // pixel setting + const lineHeight = args.lHeight != null ? `${args.lHeight}px` : '1'; + + const spec: CSSStyle = { + fontFamily: args.family, + fontWeight: args.weight, + fontStyle: args.italic ? FontStyle.ITALIC : FontStyle.NORMAL, + textDecoration: args.underline ? TextDecoration.UNDERLINE : TextDecoration.NONE, + textAlign: args.align, + fontSize: `${args.size}px`, // apply font size as a pixel setting + lineHeight, // apply line height as a pixel setting + }; + + // conditionally apply styles based on input + if (args.color) { + spec.color = args.color; + } + + return { + type: 'style', + spec, + css: inlineStyle(spec as Record), + }; + }, +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts new file mode 100644 index 0000000000000..514068da8f10c --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/index.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 { clog } from './clog'; +import { font } from './font'; +import { kibana } from './kibana'; +import { variableSet } from './var_set'; +import { variable } from './var'; +import { AnyExpressionFunctionDefinition } from '../types'; + +export const functionSpecs: AnyExpressionFunctionDefinition[] = [ + clog, + font, + kibana, + variableSet, + variable, +]; + +export * from './clog'; +export * from './font'; +export * from './kibana'; +export * from './var_set'; +export * from './var'; diff --git a/src/plugins/expressions/public/functions/kibana.ts b/src/plugins/expressions/common/expression_functions/specs/kibana.ts similarity index 52% rename from src/plugins/expressions/public/functions/kibana.ts rename to src/plugins/expressions/common/expression_functions/specs/kibana.ts index 81d0eec5f7896..2144a8aba2d19 100644 --- a/src/plugins/expressions/public/functions/kibana.ts +++ b/src/plugins/expressions/common/expression_functions/specs/kibana.ts @@ -18,47 +18,43 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from '../../common/types'; -import { KibanaContext } from '../../common/expression_types'; +import { ExpressionFunctionDefinition } from '../types'; +import { ExpressionValueSearchContext } from '../../expression_types'; -export type ExpressionFunctionKibana = ExpressionFunction< +const toArray = (query: undefined | T | T[]): T[] => + !query ? [] : Array.isArray(query) ? query : [query]; + +export type ExpressionFunctionKibana = ExpressionFunctionDefinition< 'kibana', - KibanaContext | null, + // TODO: Get rid of the `null` type below. + ExpressionValueSearchContext | null, object, - KibanaContext + ExpressionValueSearchContext >; -export const kibana = (): ExpressionFunctionKibana => ({ +export const kibana: ExpressionFunctionKibana = { name: 'kibana', type: 'kibana_context', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('expressions.functions.kibana.help', { defaultMessage: 'Gets kibana global context', }), - args: {}, - fn(context, args, handlers) { - const initialContext = handlers.getInitialContext ? handlers.getInitialContext() : {}; - - if (context && context.query) { - initialContext.query = initialContext.query.concat(context.query); - } - if (context && context.filters) { - initialContext.filters = initialContext.filters.concat(context.filters); - } - - const timeRange = initialContext.timeRange || (context ? context.timeRange : undefined); + args: {}, - return { - ...context, + fn(input, _, { search = {} }) { + const output: ExpressionValueSearchContext = { + // TODO: This spread is left here for legacy reasons, possibly Lens uses it. + // TODO: But it shouldn't be need. + ...input, type: 'kibana_context', - query: initialContext.query, - filters: initialContext.filters, - timeRange, + query: [...toArray(search.query), ...toArray((input || {}).query)], + filters: [...(search.filters || []), ...((input || {}).filters || [])], + timeRange: search.timeRange || (input ? input.timeRange : undefined), }; + + return output; }, -}); +}; diff --git a/src/plugins/expressions/public/functions/tests/__snapshots__/kibana.test.ts.snap b/src/plugins/expressions/common/expression_functions/specs/tests/__snapshots__/kibana.test.ts.snap similarity index 81% rename from src/plugins/expressions/public/functions/tests/__snapshots__/kibana.test.ts.snap rename to src/plugins/expressions/common/expression_functions/specs/tests/__snapshots__/kibana.test.ts.snap index 5a3810d8ddd93..2400f7a1f67d6 100644 --- a/src/plugins/expressions/public/functions/tests/__snapshots__/kibana.test.ts.snap +++ b/src/plugins/expressions/common/expression_functions/specs/tests/__snapshots__/kibana.test.ts.snap @@ -14,10 +14,12 @@ Object { }, }, ], - "query": Object { - "language": "lucene", - "query": "geo.src:US", - }, + "query": Array [ + Object { + "language": "lucene", + "query": "geo.src:US", + }, + ], "timeRange": Object { "from": "2", "to": "3", diff --git a/src/plugins/expressions/public/functions/tests/font.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts similarity index 99% rename from src/plugins/expressions/public/functions/tests/font.test.ts rename to src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts index f2192292d21ff..62e5fd4e0b668 100644 --- a/src/plugins/expressions/public/functions/tests/font.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/font.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { openSans } from '../../../common/fonts'; +import { openSans } from '../../../fonts'; import { font } from '../font'; import { functionWrapper } from './utils'; diff --git a/src/plugins/expressions/public/functions/tests/kibana.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts similarity index 65% rename from src/plugins/expressions/public/functions/tests/kibana.test.ts rename to src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts index b9fec590d823f..e5bd53f63c91d 100644 --- a/src/plugins/expressions/public/functions/tests/kibana.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts @@ -19,18 +19,18 @@ import { functionWrapper } from './utils'; import { kibana } from '../kibana'; -import { FunctionHandlers } from '../../../common/types'; -import { KibanaContext } from '../../../common/expression_types/kibana_context'; +import { ExecutionContext } from '../../../execution/types'; +import { KibanaContext, ExpressionValueSearchContext } from '../../../expression_types'; describe('interpreter/functions#kibana', () => { const fn = functionWrapper(kibana); - let context: Partial; - let initialContext: KibanaContext; - let handlers: FunctionHandlers; + let input: Partial; + let search: ExpressionValueSearchContext; + let context: ExecutionContext; beforeEach(() => { - context = { timeRange: { from: '0', to: '1' } }; - initialContext = { + input = { timeRange: { from: '0', to: '1' } }; + search = { type: 'kibana_context', query: { language: 'lucene', query: 'geo.src:US' }, filters: [ @@ -45,31 +45,29 @@ describe('interpreter/functions#kibana', () => { ], timeRange: { from: '2', to: '3' }, }; - handlers = { - getInitialContext: () => initialContext, + context = { + search, + getInitialInput: () => input, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, }; }); it('returns an object with the correct structure', () => { - const actual = fn(context, {}, handlers); + const actual = fn(input, {}, context); expect(actual).toMatchSnapshot(); }); - it('uses timeRange from context if not provided in initialContext', () => { - initialContext.timeRange = undefined; - const actual = fn(context, {}, handlers); + it('uses timeRange from input if not provided in search context', () => { + search.timeRange = undefined; + const actual = fn(input, {}, context); expect(actual.timeRange).toEqual({ from: '0', to: '1' }); }); - it.skip('combines query from context with initialContext', () => { - context.query = { language: 'kuery', query: 'geo.dest:CN' }; - // TODO: currently this fails & likely requires a fix in run_pipeline - const actual = fn(context, {}, handlers); - expect(actual.query).toEqual('TBD'); - }); - - it('combines filters from context with initialContext', () => { - context.filters = [ + it('combines filters from input with search context', () => { + input.filters = [ { meta: { disabled: true, @@ -79,7 +77,7 @@ describe('interpreter/functions#kibana', () => { query: { match: {} }, }, ]; - const actual = fn(context, {}, handlers); + const actual = fn(input, {}, context); expect(actual.filters).toEqual([ { meta: { diff --git a/src/plugins/expressions/public/functions/tests/utils.ts b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts similarity index 74% rename from src/plugins/expressions/public/functions/tests/utils.ts rename to src/plugins/expressions/common/expression_functions/specs/tests/utils.ts index 749b45ef0319b..bc721a772d50f 100644 --- a/src/plugins/expressions/public/functions/tests/utils.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts @@ -18,16 +18,18 @@ */ import { mapValues } from 'lodash'; -import { AnyExpressionFunction, FunctionHandlers } from '../../../common/types'; +import { AnyExpressionFunctionDefinition } from '../../types'; +import { ExecutionContext } from '../../../execution/types'; -// Takes a function spec and passes in default args, -// overriding with any provided args. -export const functionWrapper = (fnSpec: () => T) => { - const spec = fnSpec(); +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + */ +export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => { const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); return ( context: object | null, args: Record = {}, - handlers: FunctionHandlers = {} + handlers: ExecutionContext = {} as ExecutionContext ) => spec.fn(context, { ...defaultArgs, ...args }, handlers); }; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts new file mode 100644 index 0000000000000..ccf49ec918d3d --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.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 { functionWrapper } from './utils'; +import { variable } from '../var'; +import { ExecutionContext } from '../../../execution/types'; +import { KibanaContext } from '../../../expression_types'; + +describe('expression_functions', () => { + describe('var', () => { + const fn = functionWrapper(variable); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getInitialInput: () => input, + types: {}, + variables: { test: 1 }, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns the selected variable', () => { + const actual = fn(input, { name: 'test' }, context); + expect(actual).toEqual(1); + }); + + it('returns undefined if variable does not exist', () => { + const actual = fn(input, { name: 'unknown' }, context); + expect(actual).toEqual(undefined); + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts new file mode 100644 index 0000000000000..b1ae44e6f899e --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { functionWrapper } from './utils'; +import { variableSet } from '../var_set'; +import { ExecutionContext } from '../../../execution/types'; +import { KibanaContext } from '../../../expression_types'; + +describe('expression_functions', () => { + describe('var_set', () => { + const fn = functionWrapper(variableSet); + let input: Partial; + let context: ExecutionContext; + let variables: Record; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getInitialInput: () => input, + types: {}, + variables: { test: 1 }, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + + variables = context.variables; + }); + + it('updates a variable', () => { + const actual = fn(input, { name: 'test', value: 2 }, context); + expect(variables.test).toEqual(2); + expect(actual).toEqual(input); + }); + + it('sets a new variable', () => { + const actual = fn(input, { name: 'new', value: 3 }, context); + expect(variables.new).toEqual(3); + expect(actual).toEqual(input); + }); + + it('stores context if value is not set', () => { + const actual = fn(input, { name: 'test' }, context); + expect(variables.test).toEqual(input); + expect(actual).toEqual(input); + }); + }); +}); diff --git a/src/plugins/expressions/public/functions/var.ts b/src/plugins/expressions/common/expression_functions/specs/var.ts similarity index 80% rename from src/plugins/expressions/public/functions/var.ts rename to src/plugins/expressions/common/expression_functions/specs/var.ts index 9410149060216..e90a21101c557 100644 --- a/src/plugins/expressions/public/functions/var.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var.ts @@ -18,16 +18,15 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from '../../common/types'; +import { ExpressionFunctionDefinition } from '../types'; interface Arguments { name: string; } -type Context = any; -type ExpressionFunctionVar = ExpressionFunction<'var', Context, Arguments, any>; +type ExpressionFunctionVar = ExpressionFunctionDefinition<'var', unknown, Arguments, unknown>; -export const variable = (): ExpressionFunctionVar => ({ +export const variable: ExpressionFunctionVar = { name: 'var', help: i18n.translate('expressions.functions.var.help', { defaultMessage: 'Updates kibana global context', @@ -42,8 +41,8 @@ export const variable = (): ExpressionFunctionVar => ({ }), }, }, - fn(context, args, handlers) { - const variables: Record = handlers.variables; + fn(input, args, context) { + const variables: Record = context.variables; return variables[args.name]; }, -}); +}; diff --git a/src/plugins/expressions/public/functions/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts similarity index 77% rename from src/plugins/expressions/public/functions/var_set.ts rename to src/plugins/expressions/common/expression_functions/specs/var_set.ts index a10ee7a00814f..0bf89f5470b3d 100644 --- a/src/plugins/expressions/public/functions/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -18,17 +18,14 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from '../../common/types'; +import { ExpressionFunctionDefinition } from '../types'; interface Arguments { name: string; value?: any; } -type Context = any; -type ExpressionFunctionVarSet = ExpressionFunction<'var_set', Context, Arguments, Context>; - -export const variableSet = (): ExpressionFunctionVarSet => ({ +export const variableSet: ExpressionFunctionDefinition<'var_set', unknown, Arguments, unknown> = { name: 'var_set', help: i18n.translate('expressions.functions.varset.help', { defaultMessage: 'Updates kibana global context', @@ -50,9 +47,9 @@ export const variableSet = (): ExpressionFunctionVarSet => ({ }), }, }, - fn(context, args, handlers) { - const variables: Record = handlers.variables; - variables[args.name] = args.value === undefined ? context : args.value; - return context; + fn(input, args, context) { + const variables: Record = context.variables; + variables[args.name] = args.value === undefined ? input : args.value; + return input; }, -}); +}; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts new file mode 100644 index 0000000000000..b91deea36aee8 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { UnwrapPromiseOrReturn } from '@kbn/utility-types'; +import { ArgumentType } from './arguments'; +import { TypeToString } from '../types/common'; +import { ExecutionContext } from '../execution/types'; + +/** + * `ExpressionFunctionDefinition` is the interface plugins have to implement to + * register a function in `expressions` plugin. + */ +export interface ExpressionFunctionDefinition< + Name extends string, + Input, + Arguments, + Output, + Context extends ExecutionContext = ExecutionContext +> { + /** + * The name of the function, as will be used in expression. + */ + name: Name; + + /** + * Name of type of value this function outputs. + */ + type?: TypeToString>; + + /** + * List of allowed type names for input value of this function. If this + * property is set the input of function will be cast to the first possible + * type in this list. If this property is missing the input will be provided + * to the function as-is. + */ + inputTypes?: Array>; + + /** + * Specification of arguments that function supports. This list will also be + * used for autocomplete functionality when your function is being edited. + */ + args: { [key in keyof Arguments]: ArgumentType }; + + /** + * @todo What is this? + */ + aliases?: string[]; + + /** + * Help text displayed in the Expression editor. This text should be + * internationalized. + */ + help: string; + + /** + * The actual implementation of the function. + * + * @param input Output of the previous function, or initial input. + * @param args Parameters set for this function in expression. + * @param context Object with functions to perform side effects. This object + * is created for the duration of the execution of expression and is the + * same for all functions in expression chain. + */ + fn(input: Input, args: Arguments, context: Context): Output; + + /** + * @deprecated Use `inputTypes` instead. + */ + context?: { + /** + * @deprecated This is alias for `inputTypes`, use `inputTypes` instead. + */ + types: AnyExpressionFunctionDefinition['inputTypes']; + }; +} + +/** + * Type to capture every possible expression function definition. + */ +export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition; diff --git a/src/plugins/expressions/common/expression_renderers/expression_renderer.ts b/src/plugins/expressions/common/expression_renderers/expression_renderer.ts new file mode 100644 index 0000000000000..c25534c440f32 --- /dev/null +++ b/src/plugins/expressions/common/expression_renderers/expression_renderer.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 { ExpressionRenderDefinition } from './types'; + +export class ExpressionRenderer { + public readonly name: string; + public readonly displayName: string; + public readonly help: string; + public readonly validate: () => void | Error; + public readonly reuseDomNode: boolean; + public readonly render: ExpressionRenderDefinition['render']; + + constructor(config: ExpressionRenderDefinition) { + const { name, displayName, help, validate, reuseDomNode, render } = config; + + this.name = name; + this.displayName = displayName || name; + this.help = help || ''; + this.validate = validate || (() => {}); + this.reuseDomNode = Boolean(reuseDomNode); + this.render = render; + } +} diff --git a/src/plugins/expressions/common/expression_renderers/expression_renderer_registry.ts b/src/plugins/expressions/common/expression_renderers/expression_renderer_registry.ts new file mode 100644 index 0000000000000..69c0f3fad701b --- /dev/null +++ b/src/plugins/expressions/common/expression_renderers/expression_renderer_registry.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRegistry } from '../types'; +import { ExpressionRenderer } from './expression_renderer'; +import { AnyExpressionRenderDefinition } from './types'; + +export class ExpressionRendererRegistry implements IRegistry { + private readonly renderers: Map = new Map< + string, + ExpressionRenderer + >(); + + register(definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) { + if (typeof definition === 'function') definition = definition(); + const renderer = new ExpressionRenderer(definition); + this.renderers.set(renderer.name, renderer); + } + + public get(id: string): ExpressionRenderer | null { + return this.renderers.get(id) || null; + } + + public toJS(): Record { + return this.toArray().reduce( + (acc, renderer) => ({ + ...acc, + [renderer.name]: renderer, + }), + {} as Record + ); + } + + public toArray(): ExpressionRenderer[] { + return [...this.renderers.values()]; + } +} diff --git a/src/plugins/expressions/common/expression_renderers/index.ts b/src/plugins/expressions/common/expression_renderers/index.ts new file mode 100644 index 0000000000000..915e0944e9c44 --- /dev/null +++ b/src/plugins/expressions/common/expression_renderers/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 * from './types'; +export * from './expression_renderer'; +export * from './expression_renderer_registry'; diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts new file mode 100644 index 0000000000000..7b3e812eafedd --- /dev/null +++ b/src/plugins/expressions/common/expression_renderers/types.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. + */ + +export interface ExpressionRenderDefinition { + /** + * Technical name of the renderer, used as ID to identify renderer in + * expression renderer registry. This must match the name of the expression + * function that is used to create the `type: render` object. + */ + name: string; + + /** + * A user friendly name of the renderer as will be displayed to user in UI. + */ + displayName: string; + + /** + * Help text as will be displayed to user. A sentence or few about what this + * element does. + */ + help?: string; + + /** + * Used to validate the data before calling the render function. + */ + validate?: () => undefined | Error; + + /** + * Tell the renderer if the dom node should be reused, it's recreated each + * time by default. + */ + reuseDomNode: boolean; + + /** + * The function called to render the output data of an expression. + */ + render: ( + domNode: HTMLElement, + config: Config, + handlers: IInterpreterRenderHandlers + ) => void | Promise; +} + +export type AnyExpressionRenderDefinition = ExpressionRenderDefinition; + +export interface IInterpreterRenderHandlers { + /** + * Done increments the number of rendering successes + */ + done: () => void; + onDestroy: (fn: () => void) => void; + reload: () => void; + update: (params: any) => void; + event: (event: any) => void; +} diff --git a/src/plugins/expressions/common/expression_types/expression_type.test.ts b/src/plugins/expressions/common/expression_types/expression_type.test.ts new file mode 100644 index 0000000000000..a692ec9501cc5 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/expression_type.test.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ExpressionType } from './expression_type'; +import { ExpressionTypeDefinition } from './types'; +import { ExpressionValueRender } from './specs'; + +export const boolean: ExpressionTypeDefinition<'boolean', boolean> = { + name: 'boolean', + from: { + null: () => false, + number: n => Boolean(n), + string: s => Boolean(s), + }, + to: { + render: (value): ExpressionValueRender<{ text: string }> => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + }, +}; + +export const render: ExpressionTypeDefinition<'render', ExpressionValueRender> = { + name: 'render', + from: { + '*': (v: T): ExpressionValueRender => ({ + type: name, + as: 'debug', + value: v, + }), + }, +}; + +const emptyDatatableValue = { + type: 'datatable', + columns: [], + rows: [], +}; + +describe('ExpressionType', () => { + test('can create a boolean type', () => { + new ExpressionType(boolean); + }); + + describe('castsFrom()', () => { + describe('when "from" definition specifies "*" as one of its from types', () => { + test('returns true for any value', () => { + const type = new ExpressionType(render); + expect(type.castsFrom(123)).toBe(true); + expect(type.castsFrom('foo')).toBe(true); + expect(type.castsFrom(true)).toBe(true); + expect( + type.castsFrom({ + type: 'datatable', + columns: [], + rows: [], + }) + ).toBe(true); + }); + }); + }); + + describe('castsTo()', () => { + describe('when "to" definition is not specified', () => { + test('returns false for any value', () => { + const type = new ExpressionType(render); + expect(type.castsTo(123)).toBe(false); + expect(type.castsTo('foo')).toBe(false); + expect(type.castsTo(true)).toBe(false); + expect(type.castsTo(emptyDatatableValue)).toBe(false); + }); + }); + }); + + describe('from()', () => { + test('can cast from any type specified in definition', () => { + const type = new ExpressionType(boolean); + expect(type.from(1, {})).toBe(true); + expect(type.from(0, {})).toBe(false); + expect(type.from('foo', {})).toBe(true); + expect(type.from('', {})).toBe(false); + expect(type.from(null, {})).toBe(false); + + // undefined is used like null in legacy interpreter + expect(type.from(undefined, {})).toBe(false); + }); + + test('throws when casting from type that is not supported', async () => { + const type = new ExpressionType(boolean); + expect(() => type.from(emptyDatatableValue, {})).toThrowError(); + expect(() => type.from(emptyDatatableValue, {})).toThrowErrorMatchingInlineSnapshot( + `"Can not cast 'boolean' from datatable"` + ); + }); + }); + + describe('to()', () => { + test('can cast to type specified in definition', () => { + const type = new ExpressionType(boolean); + + expect(type.to(true, 'render', {})).toMatchObject({ + as: 'text', + type: 'render', + value: { + text: 'true', + }, + }); + expect(type.to(false, 'render', {})).toMatchObject({ + as: 'text', + type: 'render', + value: { + text: 'false', + }, + }); + }); + + test('throws when casting to type that is not supported', async () => { + const type = new ExpressionType(boolean); + expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowError(); + expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowErrorMatchingInlineSnapshot( + `"Can not cast object of type 'datatable' using 'boolean'"` + ); + }); + }); +}); diff --git a/src/plugins/expressions/common/type.ts b/src/plugins/expressions/common/expression_types/expression_type.ts similarity index 50% rename from src/plugins/expressions/common/type.ts rename to src/plugins/expressions/common/expression_types/expression_type.ts index c9daed9b6785a..71fa842f4dde7 100644 --- a/src/plugins/expressions/common/type.ts +++ b/src/plugins/expressions/common/expression_types/expression_type.ts @@ -17,35 +17,10 @@ * under the License. */ -import { get, identity } from 'lodash'; -import { AnyExpressionType, ExpressionValue } from './types'; - -export function getType(node: any) { - if (node == null) return 'null'; - if (typeof node === 'object') { - if (!node.type) throw new Error('Objects must have a type property'); - return node.type; - } - return typeof node; -} - -export function serializeProvider(types: any) { - function provider(key: any) { - return (context: any) => { - const type = getType(context); - const typeDef = types[type]; - const fn: any = get(typeDef, key) || identity; - return fn(context); - }; - } - - return { - serialize: provider('serialize'), - deserialize: provider('deserialize'), - }; -} +import { AnyExpressionTypeDefinition, ExpressionValue, ExpressionValueConverter } from './types'; +import { getType } from './get_type'; -export class Type { +export class ExpressionType { name: string; /** @@ -66,41 +41,53 @@ export class Type { serialize?: (value: ExpressionValue) => any; deserialize?: (serialized: any) => ExpressionValue; - constructor(private readonly config: AnyExpressionType) { - const { name, help, deserialize, serialize, validate } = config; + constructor(private readonly definition: AnyExpressionTypeDefinition) { + const { name, help, deserialize, serialize, validate } = definition; this.name = name; this.help = help || ''; this.validate = validate || (() => {}); // Optional - this.create = (config as any).create; + this.create = (definition as any).create; this.serialize = serialize; this.deserialize = deserialize; } - getToFn = (value: any) => get(this.config, ['to', value]) || get(this.config, ['to', '*']); - getFromFn = (value: any) => get(this.config, ['from', value]) || get(this.config, ['from', '*']); + getToFn = ( + typeName: string + ): undefined | ExpressionValueConverter => + !this.definition.to ? undefined : this.definition.to[typeName] || this.definition.to['*']; + + getFromFn = ( + typeName: string + ): undefined | ExpressionValueConverter => + !this.definition.from ? undefined : this.definition.from[typeName] || this.definition.from['*']; + + castsTo = (value: ExpressionValue) => typeof this.getToFn(value) === 'function'; - castsTo = (value: any) => typeof this.getToFn(value) === 'function'; - castsFrom = (value: any) => typeof this.getFromFn(value) === 'function'; + castsFrom = (value: ExpressionValue) => typeof this.getFromFn(value) === 'function'; + + to = (value: ExpressionValue, toTypeName: string, types: Record) => { + const typeName = getType(value); - to = (node: any, toTypeName: any, types: any) => { - const typeName = getType(node); if (typeName !== this.name) { throw new Error(`Can not cast object of type '${typeName}' using '${this.name}'`); } else if (!this.castsTo(toTypeName)) { throw new Error(`Can not cast '${typeName}' to '${toTypeName}'`); } - return (this.getToFn(toTypeName) as any)(node, types); + return this.getToFn(toTypeName)!(value, types); }; - from = (node: any, types: any) => { - const typeName = getType(node); - if (!this.castsFrom(typeName)) throw new Error(`Can not cast '${this.name}' from ${typeName}`); + from = (value: ExpressionValue, types: Record) => { + const typeName = getType(value); + + if (!this.castsFrom(typeName)) { + throw new Error(`Can not cast '${this.name}' from ${typeName}`); + } - return (this.getFromFn(typeName) as any)(node, types); + return this.getFromFn(typeName)!(value, types); }; } diff --git a/src/plugins/expressions/common/type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts similarity index 97% rename from src/plugins/expressions/common/type.test.ts rename to src/plugins/expressions/common/expression_types/get_type.test.ts index 94979febd623c..ba4fad5e96c49 100644 --- a/src/plugins/expressions/common/type.test.ts +++ b/src/plugins/expressions/common/expression_types/get_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getType } from './type'; +import { getType } from './get_type'; describe('getType()', () => { test('returns "null" string for null or undefined', () => { diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts new file mode 100644 index 0000000000000..9e80ffeada678 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/get_type.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. + */ + +export function getType(node: any) { + if (node == null) return 'null'; + if (typeof node === 'object') { + if (!node.type) throw new Error('Objects must have a type property'); + return node.type; + } + return typeof node; +} diff --git a/src/plugins/expressions/common/expression_types/index.ts b/src/plugins/expressions/common/expression_types/index.ts index a5d182fee75ed..5ec9a2e83583e 100644 --- a/src/plugins/expressions/common/expression_types/index.ts +++ b/src/plugins/expressions/common/expression_types/index.ts @@ -17,52 +17,8 @@ * under the License. */ -import { boolean } from './boolean'; -import { datatable } from './datatable'; -import { error } from './error'; -import { filter } from './filter'; -import { image } from './image'; -import { kibanaContext } from './kibana_context'; -import { kibanaDatatable } from './kibana_datatable'; -import { nullType } from './null'; -import { number } from './number'; -import { pointseries } from './pointseries'; -import { range } from './range'; -import { render } from './render'; -import { shape } from './shape'; -import { string } from './string'; -import { style } from './style'; - -export const typeSpecs = [ - boolean, - datatable, - error, - filter, - image, - kibanaContext, - kibanaDatatable, - nullType, - number, - pointseries, - range, - render, - shape, - string, - style, -]; - -export * from './boolean'; -export * from './datatable'; -export * from './error'; -export * from './filter'; -export * from './image'; -export * from './kibana_context'; -export * from './kibana_datatable'; -export * from './null'; -export * from './number'; -export * from './pointseries'; -export * from './range'; -export * from './render'; -export * from './shape'; -export * from './string'; -export * from './style'; +export * from './types'; +export * from './get_type'; +export * from './serialize_provider'; +export * from './expression_type'; +export * from './specs'; diff --git a/src/plugins/expressions/common/expression_types/serialize_provider.ts b/src/plugins/expressions/common/expression_types/serialize_provider.ts new file mode 100644 index 0000000000000..1cd6a24bca31b --- /dev/null +++ b/src/plugins/expressions/common/expression_types/serialize_provider.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. + */ + +import { ExpressionType } from './expression_type'; +import { ExpressionValue } from './types'; +import { getType } from './get_type'; + +const identity = (x: T) => x; + +export const serializeProvider = (types: Record) => ({ + serialize: (value: ExpressionValue) => (types[getType(value)].serialize || identity)(value), + deserialize: (value: ExpressionValue) => (types[getType(value)].deserialize || identity)(value), +}); diff --git a/src/plugins/expressions/common/expression_types/boolean.ts b/src/plugins/expressions/common/expression_types/specs/boolean.ts similarity index 83% rename from src/plugins/expressions/common/expression_types/boolean.ts rename to src/plugins/expressions/common/expression_types/specs/boolean.ts index 0ad2c14f87756..fee4608418406 100644 --- a/src/plugins/expressions/common/expression_types/boolean.ts +++ b/src/plugins/expressions/common/expression_types/specs/boolean.ts @@ -17,13 +17,13 @@ * under the License. */ -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition } from '../types'; import { Datatable } from './datatable'; -import { Render } from './render'; +import { ExpressionValueRender } from './render'; const name = 'boolean'; -export const boolean = (): ExpressionType<'boolean', boolean> => ({ +export const boolean: ExpressionTypeDefinition<'boolean', boolean> = { name, from: { null: () => false, @@ -31,7 +31,7 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({ string: s => Boolean(s), }, to: { - render: (value): Render<{ text: string }> => { + render: (value): ExpressionValueRender<{ text: string }> => { const text = `${value}`; return { type: 'render', @@ -45,4 +45,4 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({ rows: [{ value }], }), }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts similarity index 93% rename from src/plugins/expressions/common/expression_types/datatable.ts rename to src/plugins/expressions/common/expression_types/specs/datatable.ts index d58a709349c50..92254a3d02438 100644 --- a/src/plugins/expressions/common/expression_types/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -19,9 +19,9 @@ import { map, pick, zipObject } from 'lodash'; -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition } from '../types'; import { PointSeries } from './pointseries'; -import { Render } from './render'; +import { ExpressionValueRender } from './render'; const name = 'datatable'; @@ -70,7 +70,7 @@ interface RenderedDatatable { showHeader: boolean; } -export const datatable = (): ExpressionType => ({ +export const datatable: ExpressionTypeDefinition = { name, validate: table => { // TODO: Check columns types. Only string, boolean, number, date, allowed for now. @@ -115,7 +115,7 @@ export const datatable = (): ExpressionType => ({ + render: (table): ExpressionValueRender => ({ type: 'render', as: 'table', value: { @@ -143,4 +143,4 @@ export const datatable = (): ExpressionType; +export const isExpressionValueError = (value: any): value is ExpressionValueError => + getType(value) === 'error'; + /** * @deprecated * @@ -38,10 +45,10 @@ export type ExpressionValueError = ExpressionValueBoxed< */ export type InterpreterErrorType = ExpressionValueError; -export const error = (): ExpressionType<'error', ExpressionValueError> => ({ +export const error: ExpressionTypeDefinition<'error', ExpressionValueError> = { name, to: { - render: (input): Render> => { + render: (input): ExpressionValueRender> => { return { type: 'render', as: name, @@ -52,4 +59,4 @@ export const error = (): ExpressionType<'error', ExpressionValueError> => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts similarity index 90% rename from src/plugins/expressions/common/expression_types/filter.ts rename to src/plugins/expressions/common/expression_types/specs/filter.ts index 2608da6854b18..01d6b8a603db6 100644 --- a/src/plugins/expressions/common/expression_types/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition } from '../types'; const name = 'filter'; @@ -34,7 +34,7 @@ export interface Filter { query?: string | null; } -export const filter = (): ExpressionType => ({ +export const filter: ExpressionTypeDefinition = { name, from: { null: () => { @@ -47,4 +47,4 @@ export const filter = (): ExpressionType => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/image.ts b/src/plugins/expressions/common/expression_types/specs/image.ts similarity index 78% rename from src/plugins/expressions/common/expression_types/image.ts rename to src/plugins/expressions/common/expression_types/specs/image.ts index b4b6b27bbc8bc..8d89959cddb01 100644 --- a/src/plugins/expressions/common/expression_types/image.ts +++ b/src/plugins/expressions/common/expression_types/specs/image.ts @@ -17,8 +17,8 @@ * under the License. */ -import { ExpressionType } from '../types'; -import { Render } from './render'; +import { ExpressionTypeDefinition } from '../types'; +import { ExpressionValueRender } from './render'; const name = 'image'; @@ -28,10 +28,10 @@ export interface ExpressionImage { dataurl: string; } -export const image = (): ExpressionType => ({ +export const image: ExpressionTypeDefinition = { name, to: { - render: (input): Render> => { + render: (input): ExpressionValueRender> => { return { type: 'render', as: 'image', @@ -39,4 +39,4 @@ export const image = (): ExpressionType => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts new file mode 100644 index 0000000000000..31210b11f6b7a --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { boolean } from './boolean'; +import { datatable } from './datatable'; +import { error } from './error'; +import { filter } from './filter'; +import { image } from './image'; +import { kibanaContext } from './kibana_context'; +import { kibanaDatatable } from './kibana_datatable'; +import { nullType } from './null'; +import { num } from './num'; +import { number } from './number'; +import { pointseries } from './pointseries'; +import { range } from './range'; +import { render } from './render'; +import { shape } from './shape'; +import { string } from './string'; +import { style } from './style'; +import { AnyExpressionTypeDefinition } from '../types'; + +export const typeSpecs: AnyExpressionTypeDefinition[] = [ + boolean, + datatable, + error, + filter, + image, + kibanaContext, + kibanaDatatable, + nullType, + num, + number, + pointseries, + range, + render, + shape, + string, + style, +]; + +export * from './boolean'; +export * from './datatable'; +export * from './error'; +export * from './filter'; +export * from './image'; +export * from './kibana_context'; +export * from './kibana_datatable'; +export * from './null'; +export * from './num'; +export * from './number'; +export * from './pointseries'; +export * from './range'; +export * from './render'; +export * from './shape'; +export * from './string'; +export * from './style'; diff --git a/src/plugins/expressions/common/expression_types/kibana_context.ts b/src/plugins/expressions/common/expression_types/specs/kibana_context.ts similarity index 68% rename from src/plugins/expressions/common/expression_types/kibana_context.ts rename to src/plugins/expressions/common/expression_types/specs/kibana_context.ts index bcf8e2853dec8..3af7b990429c0 100644 --- a/src/plugins/expressions/common/expression_types/kibana_context.ts +++ b/src/plugins/expressions/common/expression_types/specs/kibana_context.ts @@ -17,24 +17,24 @@ * under the License. */ -import { TimeRange, Query, esFilters } from 'src/plugins/data/public'; +import { ExpressionValueBoxed } from '../types'; +import { ExecutionContextSearch } from '../../execution/types'; -const name = 'kibana_context'; -export type KIBANA_CONTEXT_NAME = 'kibana_context'; +export type ExpressionValueSearchContext = ExpressionValueBoxed< + 'kibana_context', + ExecutionContextSearch +>; -export interface KibanaContext { - type: typeof name; - query?: Query | Query[]; - filters?: esFilters.Filter[]; - timeRange?: TimeRange; -} +// TODO: These two are exported for legacy reasons - remove them eventually. +export type KIBANA_CONTEXT_NAME = 'kibana_context'; +export type KibanaContext = ExpressionValueSearchContext; -export const kibanaContext = () => ({ - name, +export const kibanaContext = { + name: 'kibana_context', from: { null: () => { return { - type: name, + type: 'kibana_context', }; }, }, @@ -45,4 +45,4 @@ export const kibanaContext = () => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts similarity index 95% rename from src/plugins/expressions/common/expression_types/kibana_datatable.ts rename to src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts index 38227d2ed6207..7742594d751de 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts @@ -18,7 +18,7 @@ */ import { map } from 'lodash'; -import { SerializedFieldFormat } from '../types/common'; +import { SerializedFieldFormat } from '../../types/common'; import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; @@ -46,7 +46,7 @@ export interface KibanaDatatable { rows: KibanaDatatableRow[]; } -export const kibanaDatatable = () => ({ +export const kibanaDatatable = { name, from: { datatable: (context: Datatable) => { @@ -72,4 +72,4 @@ export const kibanaDatatable = () => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/null.ts b/src/plugins/expressions/common/expression_types/specs/null.ts similarity index 87% rename from src/plugins/expressions/common/expression_types/null.ts rename to src/plugins/expressions/common/expression_types/specs/null.ts index 63039507870fc..60ded1dbca02f 100644 --- a/src/plugins/expressions/common/expression_types/null.ts +++ b/src/plugins/expressions/common/expression_types/specs/null.ts @@ -17,13 +17,13 @@ * under the License. */ -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition } from '../types'; const name = 'null'; -export const nullType = (): ExpressionType => ({ +export const nullType: ExpressionTypeDefinition = { name, from: { '*': () => null, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/specs/num.ts b/src/plugins/expressions/common/expression_types/specs/num.ts new file mode 100644 index 0000000000000..99b3bc3419173 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/num.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 { i18n } from '@kbn/i18n'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; +import { Datatable } from './datatable'; +import { ExpressionValueRender } from './render'; + +export type ExpressionValueNum = ExpressionValueBoxed< + 'num', + { + value: number; + } +>; + +export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = { + name: 'num', + from: { + null: () => ({ + type: 'num', + value: 0, + }), + boolean: b => ({ + type: 'num', + value: Number(b), + }), + string: n => { + const value = Number(n); + if (Number.isNaN(value)) { + throw new Error( + i18n.translate('expressions.types.number.fromStringConversionErrorMessage', { + defaultMessage: 'Can\'t typecast "{string}" string to number', + values: { + string: n, + }, + }) + ); + } + return { + type: 'num', + value, + }; + }, + '*': value => ({ + type: 'num', + value: Number(value), + }), + }, + to: { + render: ({ value }): ExpressionValueRender<{ text: string }> => { + const text = `${value}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + datatable: ({ value }): Datatable => ({ + type: 'datatable', + columns: [{ name: 'value', type: 'number' }], + rows: [{ value }], + }), + }, +}; diff --git a/src/plugins/expressions/common/expression_types/number.ts b/src/plugins/expressions/common/expression_types/specs/number.ts similarity index 86% rename from src/plugins/expressions/common/expression_types/number.ts rename to src/plugins/expressions/common/expression_types/specs/number.ts index b168391c7a65d..f346ae837adb4 100644 --- a/src/plugins/expressions/common/expression_types/number.ts +++ b/src/plugins/expressions/common/expression_types/specs/number.ts @@ -18,13 +18,13 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionType } from '../../common/types'; +import { ExpressionTypeDefinition } from '../types'; import { Datatable } from './datatable'; -import { Render } from './render'; +import { ExpressionValueRender } from './render'; const name = 'number'; -export const number = (): ExpressionType => ({ +export const number: ExpressionTypeDefinition = { name, from: { null: () => 0, @@ -45,7 +45,7 @@ export const number = (): ExpressionType => ({ }, }, to: { - render: (value: number): Render<{ text: string }> => { + render: (value: number): ExpressionValueRender<{ text: string }> => { const text = `${value}`; return { type: 'render', @@ -59,4 +59,4 @@ export const number = (): ExpressionType => ({ rows: [{ value }], }), }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/pointseries.ts b/src/plugins/expressions/common/expression_types/specs/pointseries.ts similarity index 87% rename from src/plugins/expressions/common/expression_types/pointseries.ts rename to src/plugins/expressions/common/expression_types/specs/pointseries.ts index adf2bfc67f160..9058c003b41bd 100644 --- a/src/plugins/expressions/common/expression_types/pointseries.ts +++ b/src/plugins/expressions/common/expression_types/specs/pointseries.ts @@ -17,10 +17,9 @@ * under the License. */ -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { Datatable } from './datatable'; -import { Render } from './render'; -import { ExpressionValueBoxed } from '../types/types'; +import { ExpressionValueRender } from './render'; const name = 'pointseries'; @@ -56,7 +55,7 @@ export type PointSeries = ExpressionValueBoxed< } >; -export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({ +export const pointseries: ExpressionTypeDefinition<'pointseries', PointSeries> = { name, from: { null: () => { @@ -71,7 +70,7 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({ render: ( pseries: PointSeries, types - ): Render<{ datatable: Datatable; showHeader: boolean }> => { + ): ExpressionValueRender<{ datatable: Datatable; showHeader: boolean }> => { const datatable: Datatable = types.datatable.from(pseries, types); return { type: 'render', @@ -83,4 +82,4 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/range.ts b/src/plugins/expressions/common/expression_types/specs/range.ts similarity index 83% rename from src/plugins/expressions/common/expression_types/range.ts rename to src/plugins/expressions/common/expression_types/specs/range.ts index 082056c909988..3d7170cf715d7 100644 --- a/src/plugins/expressions/common/expression_types/range.ts +++ b/src/plugins/expressions/common/expression_types/specs/range.ts @@ -17,8 +17,8 @@ * under the License. */ -import { ExpressionType } from '../types'; -import { Render } from '.'; +import { ExpressionTypeDefinition } from '../types'; +import { ExpressionValueRender } from '.'; const name = 'range'; @@ -28,7 +28,7 @@ export interface Range { to: number; } -export const range = (): ExpressionType => ({ +export const range: ExpressionTypeDefinition = { name, from: { null: (): Range => { @@ -40,7 +40,7 @@ export const range = (): ExpressionType => ({ }, }, to: { - render: (value: Range): Render<{ text: string }> => { + render: (value: Range): ExpressionValueRender<{ text: string }> => { const text = `from ${value.from} to ${value.to}`; return { type: 'render', @@ -49,4 +49,4 @@ export const range = (): ExpressionType => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/render.ts b/src/plugins/expressions/common/expression_types/specs/render.ts similarity index 71% rename from src/plugins/expressions/common/expression_types/render.ts rename to src/plugins/expressions/common/expression_types/specs/render.ts index 3d6852b897508..d0af59ba6d718 100644 --- a/src/plugins/expressions/common/expression_types/render.ts +++ b/src/plugins/expressions/common/expression_types/specs/render.ts @@ -17,15 +17,14 @@ * under the License. */ -import { ExpressionType } from '../types'; -import { ExpressionValueBoxed } from '../types/types'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; const name = 'render'; /** * Represents an object that is intended to be rendered. */ -export type Render = ExpressionValueBoxed< +export type ExpressionValueRender = ExpressionValueBoxed< typeof name, { as: string; @@ -33,13 +32,20 @@ export type Render = ExpressionValueBoxed< } >; -export const render = (): ExpressionType> => ({ +/** + * @deprecated + * + * Use `ExpressionValueRender` instead. + */ +export type Render = ExpressionValueRender; + +export const render: ExpressionTypeDefinition> = { name, from: { - '*': (v: T): Render => ({ + '*': (v: T): ExpressionValueRender => ({ type: name, as: 'debug', value: v, }), }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/shape.ts b/src/plugins/expressions/common/expression_types/specs/shape.ts similarity index 83% rename from src/plugins/expressions/common/expression_types/shape.ts rename to src/plugins/expressions/common/expression_types/specs/shape.ts index fd176e188a47b..315838043cb49 100644 --- a/src/plugins/expressions/common/expression_types/shape.ts +++ b/src/plugins/expressions/common/expression_types/specs/shape.ts @@ -17,12 +17,12 @@ * under the License. */ -import { ExpressionType } from '../types'; -import { Render } from './render'; +import { ExpressionTypeDefinition } from '../types'; +import { ExpressionValueRender } from './render'; const name = 'shape'; -export const shape = (): ExpressionType> => ({ +export const shape: ExpressionTypeDefinition> = { name: 'shape', to: { render: input => { @@ -33,4 +33,4 @@ export const shape = (): ExpressionType> => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/string.ts b/src/plugins/expressions/common/expression_types/specs/string.ts similarity index 83% rename from src/plugins/expressions/common/expression_types/string.ts rename to src/plugins/expressions/common/expression_types/specs/string.ts index 52b7c35189612..d46f0e5f6b7c2 100644 --- a/src/plugins/expressions/common/expression_types/string.ts +++ b/src/plugins/expressions/common/expression_types/specs/string.ts @@ -17,13 +17,13 @@ * under the License. */ -import { ExpressionType } from '../types'; +import { ExpressionTypeDefinition } from '../types'; import { Datatable } from './datatable'; -import { Render } from './render'; +import { ExpressionValueRender } from './render'; const name = 'string'; -export const string = (): ExpressionType => ({ +export const string: ExpressionTypeDefinition = { name, from: { null: () => '', @@ -31,7 +31,7 @@ export const string = (): ExpressionType => ({ number: n => String(n), }, to: { - render: (text: T): Render<{ text: T }> => { + render: (text: T): ExpressionValueRender<{ text: T }> => { return { type: 'render', as: 'text', @@ -44,4 +44,4 @@ export const string = (): ExpressionType => ({ rows: [{ value }], }), }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/style.ts b/src/plugins/expressions/common/expression_types/specs/style.ts similarity index 82% rename from src/plugins/expressions/common/expression_types/style.ts rename to src/plugins/expressions/common/expression_types/specs/style.ts index d93893d25c11c..57c12e2829fa0 100644 --- a/src/plugins/expressions/common/expression_types/style.ts +++ b/src/plugins/expressions/common/expression_types/specs/style.ts @@ -17,11 +17,12 @@ * under the License. */ -import { ExpressionType, ExpressionTypeStyle } from '../types'; +import { ExpressionTypeDefinition } from '../types'; +import { ExpressionTypeStyle } from '../../types/style'; const name = 'style'; -export const style = (): ExpressionType => ({ +export const style: ExpressionTypeDefinition = { name, from: { null: () => { @@ -32,4 +33,4 @@ export const style = (): ExpressionType => ({ }; }, }, -}); +}; diff --git a/src/plugins/expressions/common/expression_types/tests/number.test.ts b/src/plugins/expressions/common/expression_types/specs/tests/number.test.ts similarity index 91% rename from src/plugins/expressions/common/expression_types/tests/number.test.ts rename to src/plugins/expressions/common/expression_types/specs/tests/number.test.ts index 3336a1384ea79..c643ae849c034 100644 --- a/src/plugins/expressions/common/expression_types/tests/number.test.ts +++ b/src/plugins/expressions/common/expression_types/specs/tests/number.test.ts @@ -21,7 +21,7 @@ import { number } from '../number'; describe('number', () => { it('should fail when typecasting not numeric string to number', () => { - expect(() => number().from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot( + expect(() => number.from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot( `"Can't typecast \\"123test\\" string to number"` ); }); diff --git a/src/plugins/expressions/common/types/types.ts b/src/plugins/expressions/common/expression_types/types.ts similarity index 93% rename from src/plugins/expressions/common/types/types.ts rename to src/plugins/expressions/common/expression_types/types.ts index e7b30d24fa6eb..3817530c27029 100644 --- a/src/plugins/expressions/common/types/types.ts +++ b/src/plugins/expressions/common/expression_types/types.ts @@ -34,7 +34,7 @@ export type ExpressionValueConverter; +export type AnyExpressionTypeDefinition = ExpressionTypeDefinition; diff --git a/src/plugins/expressions/common/index.ts b/src/plugins/expressions/common/index.ts index f4bd448c19772..f03fdcbda7ff1 100644 --- a/src/plugins/expressions/common/index.ts +++ b/src/plugins/expressions/common/index.ts @@ -17,6 +17,13 @@ * under the License. */ -export * from './type'; export * from './types'; +export * from './ast'; +export * from './fonts'; export * from './expression_types'; +export * from './expression_functions'; +export * from './expression_renderers'; +export * from './executor'; +export * from './execution'; +export * from './service'; +export * from './util'; diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts new file mode 100644 index 0000000000000..502d88ac955ae --- /dev/null +++ b/src/plugins/expressions/common/mocks.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 { ExecutionContext } from './execution/types'; + +export const createMockExecutionContext = ( + extraContext: ExtraContext = {} as ExtraContext +): ExecutionContext & ExtraContext => { + const executionContext: ExecutionContext = { + getInitialInput: jest.fn(), + variables: {}, + types: {}, + abortSignal: { + aborted: false, + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + onabort: jest.fn(), + removeEventListener: jest.fn(), + }, + inspectorAdapters: { + requests: {} as any, + data: {} as any, + }, + search: {}, + }; + + return { + ...executionContext, + ...extraContext, + }; +}; diff --git a/src/plugins/expressions/common/service/expressions_services.test.ts b/src/plugins/expressions/common/service/expressions_services.test.ts new file mode 100644 index 0000000000000..c9687192481c6 --- /dev/null +++ b/src/plugins/expressions/common/service/expressions_services.test.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionsService } from './expressions_services'; + +describe('ExpressionsService', () => { + test('can instantiate', () => { + new ExpressionsService(); + }); + + test('returns expected setup contract', () => { + const expressions = new ExpressionsService(); + + expect(expressions.setup()).toMatchObject({ + getFunctions: expect.any(Function), + registerFunction: expect.any(Function), + registerType: expect.any(Function), + registerRenderer: expect.any(Function), + run: expect.any(Function), + }); + }); + + test('returns expected start contract', () => { + const expressions = new ExpressionsService(); + expressions.setup(); + + expect(expressions.start()).toMatchObject({ + getFunctions: expect.any(Function), + run: expect.any(Function), + }); + }); + + test('has pre-installed default functions', () => { + const expressions = new ExpressionsService(); + + expect(typeof expressions.setup().getFunctions().var_set).toBe('object'); + }); +}); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts new file mode 100644 index 0000000000000..8543fbe0fced2 --- /dev/null +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Executor } from '../executor'; +import { ExpressionRendererRegistry } from '../expression_renderers'; +import { ExpressionAstExpression } from '../ast'; + +export type ExpressionsServiceSetup = ReturnType; +export type ExpressionsServiceStart = ReturnType; + +/** + * `ExpressionsService` class is used for multiple purposes: + * + * 1. It implements the same Expressions service that can be used on both: + * (1) server-side and (2) browser-side. + * 2. It implements the same Expressions service that users can fork/clone, + * thus have their own instance of the Expressions plugin. + * 3. `ExpressionsService` defines the public contracts of *setup* and *start* + * Kibana Platform life-cycles for ease-of-use on server-side and browser-side. + * 4. `ExpressionsService` creates a bound version of all exported contract functions. + * 5. Functions are bound the way there are: + * + * ```ts + * registerFunction = (...args: Parameters + * ): ReturnType => this.executor.registerFunction(...args); + * ``` + * + * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. + */ +export class ExpressionsService { + public readonly executor = Executor.createWithDefaults(); + public readonly renderers = new ExpressionRendererRegistry(); + + /** + * Register an expression function, which will be possible to execute as + * part of the expression pipeline. + * + * Below we register a function which simply sleeps for given number of + * milliseconds to delay the execution and outputs its input as-is. + * + * ```ts + * expressions.registerFunction({ + * name: 'sleep', + * args: { + * time: { + * aliases: ['_'], + * help: 'Time in milliseconds for how long to sleep', + * types: ['number'], + * }, + * }, + * help: '', + * fn: async (input, args, context) => { + * await new Promise(r => setTimeout(r, args.time)); + * return input; + * }, + * } + * ``` + * + * The actual function is defined in the `fn` key. The function can be *async*. + * It receives three arguments: (1) `input` is the output of the previous function + * or the initial input of the expression if the function is first in chain; + * (2) `args` are function arguments as defined in expression string, that can + * be edited by user (e.g in case of Canvas); (3) `context` is a shared object + * passed to all functions that can be used for side-effects. + */ + public readonly registerFunction = ( + ...args: Parameters + ): ReturnType => this.executor.registerFunction(...args); + + /** + * Executes expression string or a parsed expression AST and immediately + * returns the result. + * + * Below example will execute `sleep 100 | clog` expression with `123` initial + * input to the first function. + * + * ```ts + * expressions.run('sleep 100 | clog', 123); + * ``` + * + * - `sleep 100` will delay execution by 100 milliseconds and pass the `123` input as + * its output. + * - `clog` will print to console `123` and pass it as its output. + * - The final result of the execution will be `123`. + * + * Optionally, you can pass an object as the third argument which will be used + * to extend the `ExecutionContext`—an object passed to each function + * as the third argument, that allows functions to perform side-effects. + * + * ```ts + * expressions.run('...', null, { elasticsearchClient }); + * ``` + */ + public readonly run = < + Input, + Output, + ExtraContext extends Record = Record + >( + ast: string | ExpressionAstExpression, + input: Input, + context?: ExtraContext + ): Promise => this.executor.run(ast, input, context); + + public setup() { + const { executor, renderers, registerFunction, run } = this; + + const getFunction = executor.getFunction.bind(executor); + const getFunctions = executor.getFunctions.bind(executor); + const getRenderer = renderers.get.bind(renderers); + const getRenderers = renderers.toJS.bind(renderers); + const getType = executor.getType.bind(executor); + const getTypes = executor.getTypes.bind(executor); + const registerRenderer = renderers.register.bind(renderers); + const registerType = executor.registerType.bind(executor); + + return { + getFunction, + getFunctions, + getRenderer, + getRenderers, + getType, + getTypes, + registerFunction, + registerRenderer, + registerType, + run, + }; + } + + public start() { + const { executor, renderers, run } = this; + + const getFunction = executor.getFunction.bind(executor); + const getFunctions = executor.getFunctions.bind(executor); + const getRenderer = renderers.get.bind(renderers); + const getRenderers = renderers.toJS.bind(renderers); + const getType = executor.getType.bind(executor); + const getTypes = executor.getTypes.bind(executor); + + return { + getFunction, + getFunctions, + getRenderer, + getRenderers, + getType, + getTypes, + run, + }; + } + + public stop() {} +} diff --git a/src/plugins/expressions/common/service/index.ts b/src/plugins/expressions/common/service/index.ts new file mode 100644 index 0000000000000..219da048251f7 --- /dev/null +++ b/src/plugins/expressions/common/service/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 './expressions_services'; diff --git a/src/plugins/expressions/public/registries/registry.ts b/src/plugins/expressions/common/test_helpers/create_unit_test_executor.ts similarity index 69% rename from src/plugins/expressions/public/registries/registry.ts rename to src/plugins/expressions/common/test_helpers/create_unit_test_executor.ts index fe149116fbf14..1414db4f50b27 100644 --- a/src/plugins/expressions/public/registries/registry.ts +++ b/src/plugins/expressions/common/test_helpers/create_unit_test_executor.ts @@ -17,26 +17,15 @@ * under the License. */ -export class Registry { - private data: Record = {}; +import { Executor } from '../executor'; +import { functionTestSpecs } from './expression_functions'; - set(id: string, item: T) { - this.data[id] = item; - } - - get(id: string): T | null { - return this.data[id] || null; - } +export const createUnitTestExecutor = () => { + const executor = Executor.createWithDefaults(); - toJS(): Record { - return { ...this.data }; + for (const func of functionTestSpecs) { + executor.registerFunction(func); } - toArray(): T[] { - return Object.values(this.data); - } - - reset() { - this.data = {}; - } -} + return executor; +}; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/access.ts b/src/plugins/expressions/common/test_helpers/expression_functions/access.ts new file mode 100644 index 0000000000000..72adf95745f7d --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/access.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ExpressionFunctionDefinition } from '../../expression_functions'; + +export const access: ExpressionFunctionDefinition<'access', any, { key: string }, any> = { + name: 'access', + help: 'Access key on input object or return the input, if it is not an object', + args: { + key: { + aliases: ['_'], + help: 'Key on input object', + types: ['string'], + }, + }, + fn: (input, { key }, context) => { + return !input ? input : typeof input === 'object' ? input[key] : input; + }, +}; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/add.ts b/src/plugins/expressions/common/test_helpers/expression_functions/add.ts new file mode 100644 index 0000000000000..5c031a64e4cc5 --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/add.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 { ExpressionFunctionDefinition } from '../../expression_functions'; +import { ExpressionValueNum } from '../../expression_types'; + +export const add: ExpressionFunctionDefinition< + 'add', + ExpressionValueNum, + { val: number | null | string }, + ExpressionValueNum +> = { + name: 'add', + help: 'This function adds a number to input', + inputTypes: ['num'], + args: { + val: { + default: 0, + aliases: ['_'], + help: 'Number to add to input', + types: ['null', 'number', 'string'], + }, + }, + fn: ({ value: value1 }, { val: input2 }, context) => { + const value2 = !input2 + ? 0 + : typeof input2 === 'object' + ? (input2 as any).value + : Number(input2); + + return { + type: 'num', + value: value1 + value2, + }; + }, +}; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/error.ts b/src/plugins/expressions/common/test_helpers/expression_functions/error.ts new file mode 100644 index 0000000000000..e672bccad4720 --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/error.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ExpressionFunctionDefinition } from '../../expression_functions'; +import { ExpressionValueNum } from '../../expression_types'; + +export const error: ExpressionFunctionDefinition< + 'error', + ExpressionValueNum, + { message: string }, + ExpressionValueNum +> = { + name: 'error', + help: 'This function always throws an error', + args: { + message: { + default: 'Unknown', + aliases: ['_'], + help: 'Number to add to input', + types: ['string'], + }, + }, + fn: (input, args, context) => { + throw new Error(args.message); + }, +}; diff --git a/src/plugins/expressions/public/registries/type_registry.ts b/src/plugins/expressions/common/test_helpers/expression_functions/index.ts similarity index 64% rename from src/plugins/expressions/public/registries/type_registry.ts rename to src/plugins/expressions/common/test_helpers/expression_functions/index.ts index 6dfb71f1006ce..5b141983b7bec 100644 --- a/src/plugins/expressions/public/registries/type_registry.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/index.ts @@ -17,13 +17,19 @@ * under the License. */ -import { Registry } from './registry'; -import { Type } from '../../common/type'; -import { AnyExpressionType } from '../../common/types'; +import { access } from './access'; +import { add } from './add'; +import { error } from './error'; +import { introspectContext } from './introspect_context'; +import { mult } from './mult'; +import { sleep } from './sleep'; +import { AnyExpressionFunctionDefinition } from '../../expression_functions'; -export class TypesRegistry extends Registry { - register(typeDefinition: AnyExpressionType | (() => AnyExpressionType)) { - const type = new Type(typeof typeDefinition === 'object' ? typeDefinition : typeDefinition()); - this.set(type.name, type); - } -} +export const functionTestSpecs: AnyExpressionFunctionDefinition[] = [ + access, + add, + error, + introspectContext, + mult, + sleep, +]; diff --git a/src/plugins/expressions/public/serialize_provider.ts b/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts similarity index 64% rename from src/plugins/expressions/public/serialize_provider.ts rename to src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts index f5a69ed52ed52..0e2b356b5c5a9 100644 --- a/src/plugins/expressions/public/serialize_provider.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/introspect_context.ts @@ -17,21 +17,26 @@ * under the License. */ -import { get, identity } from 'lodash'; -import { getType } from '../common/type'; +import { ExpressionFunctionDefinition } from '../../expression_functions'; -export function serializeProvider(types: any) { - return { - serialize: provider('serialize'), - deserialize: provider('deserialize'), - }; - - function provider(key: any) { - return (context: any) => { - const type = getType(context); - const typeDef = types[type]; - const fn: any = get(typeDef, key) || identity; - return fn(context); +export const introspectContext: ExpressionFunctionDefinition< + 'introspectContext', + any, + { key: string }, + any +> = { + name: 'introspectContext', + args: { + key: { + help: 'Context key to introspect', + types: ['string'], + }, + }, + help: '', + fn: (input, args, context) => { + return { + type: 'any', + result: (context as any)[args.key], }; - } -} + }, +}; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/mult.ts b/src/plugins/expressions/common/test_helpers/expression_functions/mult.ts new file mode 100644 index 0000000000000..7a220188c6cea --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/mult.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 { ExpressionFunctionDefinition } from '../../expression_functions'; +import { ExpressionValueNum } from '../../expression_types'; + +export const mult: ExpressionFunctionDefinition< + 'mult', + ExpressionValueNum, + { val: number }, + ExpressionValueNum +> = { + name: 'mult', + help: 'This function multiplies input by a number', + args: { + val: { + default: 0, + help: 'Number to multiply input by', + types: ['number'], + }, + }, + fn: ({ value }, args, context) => { + return { + type: 'num', + value: value * args.val, + }; + }, +}; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts b/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts new file mode 100644 index 0000000000000..e9ff6e0698560 --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/sleep.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionFunctionDefinition } from '../../expression_functions'; + +export const sleep: ExpressionFunctionDefinition<'sleep', any, { time: number }, any> = { + name: 'sleep', + args: { + time: { + aliases: ['_'], + help: 'Time in milliseconds for how long to sleep', + types: ['number'], + }, + }, + help: '', + fn: async (input, args, context) => { + await new Promise(r => setTimeout(r, args.time)); + return input; + }, +}; diff --git a/src/plugins/expressions/common/test_helpers/index.ts b/src/plugins/expressions/common/test_helpers/index.ts new file mode 100644 index 0000000000000..c1e68496140e7 --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/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 './create_unit_test_executor'; diff --git a/src/plugins/expressions/common/types/common.ts b/src/plugins/expressions/common/types/common.ts index 68df29ee69846..f532f9708940e 100644 --- a/src/plugins/expressions/common/types/common.ts +++ b/src/plugins/expressions/common/types/common.ts @@ -17,6 +17,8 @@ * under the License. */ +import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; + /** * This can convert a type into a known Expression string representation of * that type. For example, `TypeToString` will resolve to `'datatable'`. @@ -45,7 +47,7 @@ export type KnownTypeToString = * * `someArgument: Promise` results in `types: ['boolean', 'string']` */ -export type TypeString = KnownTypeToString>; +export type TypeString = KnownTypeToString>; /** * Types used in Expressions that don't map to a primitive cleanly: @@ -54,11 +56,6 @@ export type TypeString = KnownTypeToString>; */ export type UnmappedTypeStrings = 'date' | 'filter'; -/** - * Utility type: extracts returned type from a Promise. - */ -export type UnwrapPromise = T extends Promise ? P : T; - /** * JSON representation of a field formatter configuration. * Is used to carry information about how to format data in diff --git a/src/plugins/expressions/common/types/functions.ts b/src/plugins/expressions/common/types/functions.ts deleted file mode 100644 index 5ead129398e42..0000000000000 --- a/src/plugins/expressions/common/types/functions.ts +++ /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 { ArgumentType } from './arguments'; -import { TypeToString, UnwrapPromise } from './common'; - -/** - * A generic type which represents an Expression Function definition. - */ -export interface ExpressionFunction { - /** Arguments for the Function */ - args: { [key in keyof Arguments]: ArgumentType }; - aliases?: string[]; - context?: { - types: Array>; - }; - /** Help text displayed in the Expression editor */ - help: string; - /** The name of the Function */ - name: Name; - /** The type of the Function */ - type?: TypeToString>; - /** The implementation of the Function */ - fn(context: Context, args: Arguments, handlers: FunctionHandlers): Return; -} - -// TODO: Handlers can be passed to the `fn` property of the Function. At the moment, these Functions -// are not strongly defined. -export interface FunctionHandlers { - [key: string]: (...args: any) => any; -} - -export type AnyExpressionFunction = ExpressionFunction; diff --git a/src/plugins/expressions/common/types/index.ts b/src/plugins/expressions/common/types/index.ts index d3be079604dee..4313ea934d038 100644 --- a/src/plugins/expressions/common/types/index.ts +++ b/src/plugins/expressions/common/types/index.ts @@ -17,34 +17,13 @@ * under the License. */ -export * from './types'; - export { TypeToString, KnownTypeToString, TypeString, UnmappedTypeStrings, - UnwrapPromise, SerializedFieldFormat, } from './common'; export * from './style'; - -export { ArgumentType } from './arguments'; - -export { ExpressionFunction, AnyExpressionFunction, FunctionHandlers } from './functions'; - -export type ExpressionArgAST = string | boolean | number | ExpressionAST; - -export interface ExpressionFunctionAST { - type: 'function'; - function: string; - arguments: { - [key: string]: ExpressionArgAST[]; - }; -} - -export interface ExpressionAST { - type: 'expression'; - chain: ExpressionFunctionAST[]; -} +export * from './registry'; diff --git a/src/plugins/expressions/common/types/registry.ts b/src/plugins/expressions/common/types/registry.ts new file mode 100644 index 0000000000000..ba4bff3b8f1bb --- /dev/null +++ b/src/plugins/expressions/common/types/registry.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 interface IRegistry { + get(id: string): T | null; + + toJS(): Record; + + toArray(): T[]; +} diff --git a/src/plugins/expressions/public/create_error.ts b/src/plugins/expressions/common/util/create_error.ts similarity index 100% rename from src/plugins/expressions/public/create_error.ts rename to src/plugins/expressions/common/util/create_error.ts diff --git a/src/plugins/expressions/common/util/get_by_alias.ts b/src/plugins/expressions/common/util/get_by_alias.ts new file mode 100644 index 0000000000000..6868abb5da923 --- /dev/null +++ b/src/plugins/expressions/common/util/get_by_alias.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. + */ + +/** + * This is used for looking up function/argument definitions. It looks through + * the given object/array for a case-insensitive match, which could be either the + * `name` itself, or something under the `aliases` property. + */ +export function getByAlias( + node: T[] | Record, + nodeName: string +): T | undefined { + const lowerCaseName = nodeName.toLowerCase(); + return Object.values(node).find(({ name, aliases }) => { + if (!name) return false; + if (name.toLowerCase() === lowerCaseName) return true; + return (aliases || []).some(alias => { + return alias.toLowerCase() === lowerCaseName; + }); + }); +} diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts new file mode 100644 index 0000000000000..ee677d54ce968 --- /dev/null +++ b/src/plugins/expressions/common/util/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 * from './create_error'; +export * from './get_by_alias'; diff --git a/src/plugins/expressions/index.ts b/src/plugins/expressions/index.ts new file mode 100644 index 0000000000000..a9794d9e4647a --- /dev/null +++ b/src/plugins/expressions/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 './common'; diff --git a/src/plugins/expressions/public/batched_fetch.test.ts b/src/plugins/expressions/public/batched_fetch.test.ts deleted file mode 100644 index 7273be872a725..0000000000000 --- a/src/plugins/expressions/public/batched_fetch.test.ts +++ /dev/null @@ -1,84 +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 { batchedFetch, Request } from './batched_fetch'; -import { defer } from '../../kibana_utils/public'; -import { Subject } from 'rxjs'; - -const serialize = (o: any) => JSON.stringify(o); - -const fetchStreaming = jest.fn(({ body }) => { - const { functions } = JSON.parse(body); - const { promise, resolve } = defer(); - const stream = new Subject(); - - setTimeout(() => { - functions.map(({ id, functionName, context, args }: Request) => - stream.next( - JSON.stringify({ - id, - statusCode: context, - result: Number(context) >= 400 ? { err: {} } : `${functionName}${context}${args}`, - }) + '\n' - ) - ); - resolve(); - }, 1); - - return { promise, stream }; -}) as any; - -describe('batchedFetch', () => { - it('resolves the correct promise', async () => { - const ajax = batchedFetch({ fetchStreaming, serialize, ms: 1 }); - - const result = await Promise.all([ - ajax({ functionName: 'a', context: 1, args: 'aaa' }), - ajax({ functionName: 'b', context: 2, args: 'bbb' }), - ]); - - expect(result).toEqual(['a1aaa', 'b2bbb']); - }); - - it('dedupes duplicate calls', async () => { - const ajax = batchedFetch({ fetchStreaming, serialize, ms: 1 }); - - const result = await Promise.all([ - ajax({ functionName: 'a', context: 1, args: 'aaa' }), - ajax({ functionName: 'b', context: 2, args: 'bbb' }), - ajax({ functionName: 'a', context: 1, args: 'aaa' }), - ajax({ functionName: 'a', context: 1, args: 'aaa' }), - ]); - - expect(result).toEqual(['a1aaa', 'b2bbb', 'a1aaa', 'a1aaa']); - expect(fetchStreaming).toHaveBeenCalledTimes(2); - }); - - it('rejects responses whose statusCode is >= 300', async () => { - const ajax = batchedFetch({ fetchStreaming, serialize, ms: 1 }); - - const result = await Promise.all([ - ajax({ functionName: 'a', context: 500, args: 'aaa' }).catch(() => 'fail'), - ajax({ functionName: 'b', context: 400, args: 'bbb' }).catch(() => 'fail'), - ajax({ functionName: 'c', context: 200, args: 'ccc' }), - ]); - - expect(result).toEqual(['fail', 'fail', 'c200ccc']); - }); -}); diff --git a/src/plugins/expressions/public/batched_fetch.ts b/src/plugins/expressions/public/batched_fetch.ts deleted file mode 100644 index 6a155b7d42b72..0000000000000 --- a/src/plugins/expressions/public/batched_fetch.ts +++ /dev/null @@ -1,144 +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 { filter, map } from 'rxjs/operators'; -// eslint-disable-next-line -import { split, BfetchPublicContract } from '../../bfetch/public'; -import { defer } from '../../kibana_utils/public'; - -export interface Options { - fetchStreaming: BfetchPublicContract['fetchStreaming']; - serialize: any; - ms?: number; -} - -export type Batch = Record; - -export interface BatchEntry { - future: any; - request: Request; -} - -export interface Request { - id?: number; - functionName: string; - args: any; - context: string; -} - -/** - * Create a function which executes an Expression function on the - * server as part of a larger batch of executions. - */ -export function batchedFetch({ fetchStreaming, serialize, ms = 10 }: Options) { - // Uniquely identifies each function call in a batch operation - // so that the appropriate promise can be resolved / rejected later. - let id = 0; - - // A map like { id: { future, request } }, which is used to - // track all of the function calls in a batch operation. - let batch: Batch = {}; - let timeout: any; - - const nextId = () => ++id; - - const reset = () => { - id = 0; - batch = {}; - timeout = undefined; - }; - - const runBatch = () => { - processBatch(fetchStreaming, batch); - reset(); - }; - - return ({ functionName, context, args }: any) => { - if (!timeout) { - timeout = setTimeout(runBatch, ms); - } - - const request: Request = { - functionName, - args, - context: serialize(context), - }; - - // Check to see if this is a duplicate server function. - const duplicate: any = Object.values(batch).find((batchedRequest: any) => - _.isMatch(batchedRequest.request, request) - ); - - // If it is, just return the promise of the duplicated request. - if (duplicate) { - return duplicate.future.promise; - } - - // If not, create a new promise, id, and add it to the batched collection. - const future = defer(); - const newId = nextId(); - request.id = newId; - - batch[newId] = { - future, - request, - }; - - return future.promise; - }; -} - -/** - * Runs the specified batch of functions on the server, then resolves - * the related promises. - */ -async function processBatch(fetchStreaming: BfetchPublicContract['fetchStreaming'], batch: Batch) { - const { stream } = fetchStreaming({ - url: `/api/interpreter/fns`, - body: JSON.stringify({ - functions: Object.values(batch).map(({ request }) => request), - }), - }); - - stream - .pipe( - split('\n'), - filter(Boolean), - map((json: string) => JSON.parse(json)) - ) - .subscribe((message: any) => { - const { id, statusCode, result } = message; - const { future } = batch[id]; - - if (statusCode >= 400) { - future.reject(result); - } else { - future.resolve(result); - } - }); - - try { - await stream.toPromise(); - } catch (error) { - Object.values(batch).forEach(({ future }) => { - future.reject(error); - }); - } -} diff --git a/src/plugins/expressions/public/execute.test.ts b/src/plugins/expressions/public/execute.test.ts index 6700ec38df940..2f2a303bad4c4 100644 --- a/src/plugins/expressions/public/execute.test.ts +++ b/src/plugins/expressions/public/execute.test.ts @@ -17,14 +17,13 @@ * under the License. */ -import { fromExpression } from '@kbn/interpreter/common'; import { execute, ExpressionDataHandler } from './execute'; -import { ExpressionAST } from '../common/types'; +import { ExpressionAstExpression, parseExpression } from '../common'; jest.mock('./services', () => ({ getInterpreter: () => { return { - interpretAst: async (expression: ExpressionAST) => { + interpretAst: async (expression: ExpressionAstExpression) => { return {}; }, }; @@ -55,7 +54,7 @@ describe('ExpressionDataHandler', () => { }); it('accepts expression AST', () => { - const expressionAST = fromExpression(expressionString) as ExpressionAST; + const expressionAST = parseExpression(expressionString) as ExpressionAstExpression; const expressionDataHandler = new ExpressionDataHandler(expressionAST, {}); expect(expressionDataHandler.getExpression()).toEqual(expressionString); expect(expressionDataHandler.getAst()).toEqual(expressionAST); @@ -70,7 +69,7 @@ describe('ExpressionDataHandler', () => { it('allows passing in search context', () => { const expressionDataHandler = new ExpressionDataHandler(expressionString, { - searchContext: { type: 'kibana_context', filters: [] }, + searchContext: { filters: [] }, }); expect(expressionDataHandler.getExpression()).toEqual(expressionString); }); diff --git a/src/plugins/expressions/public/execute.ts b/src/plugins/expressions/public/execute.ts index 89ef272a0d023..c07fb9ad0549c 100644 --- a/src/plugins/expressions/public/execute.ts +++ b/src/plugins/expressions/public/execute.ts @@ -17,11 +17,15 @@ * under the License. */ -import { fromExpression, toExpression } from '@kbn/interpreter/target/common'; import { DataAdapter, RequestAdapter, Adapters } from '../../inspector/public'; import { getInterpreter } from './services'; -import { IExpressionLoaderParams, IInterpreterResult } from './types'; -import { ExpressionAST } from '../common/types'; +import { IExpressionLoaderParams } from './types'; +import { + ExpressionAstExpression, + parseExpression, + formatExpression, + ExpressionValue, +} from '../common'; /** * The search context describes a specific context (filters, time range and query) @@ -34,48 +38,42 @@ import { ExpressionAST } from '../common/types'; export class ExpressionDataHandler { private abortController: AbortController; private expression: string; - private ast: ExpressionAST; + private ast: ExpressionAstExpression; private inspectorAdapters: Adapters; - private promise: Promise; + private promise: Promise; public isPending: boolean = true; - constructor(expression: string | ExpressionAST, params: IExpressionLoaderParams) { + constructor(expression: string | ExpressionAstExpression, params: IExpressionLoaderParams) { if (typeof expression === 'string') { this.expression = expression; - this.ast = fromExpression(expression) as ExpressionAST; + this.ast = parseExpression(expression); } else { this.ast = expression; - this.expression = toExpression(this.ast); + this.expression = formatExpression(this.ast); } this.abortController = new AbortController(); this.inspectorAdapters = params.inspectorAdapters || this.getActiveInspectorAdapters(); - const getInitialContext = () => ({ - type: 'kibana_context', - ...params.searchContext, - }); - - const defaultContext = { type: 'null' }; - + const defaultInput = { type: 'null' }; const interpreter = getInterpreter(); this.promise = interpreter - .interpretAst(this.ast, params.context || defaultContext, { - getInitialContext, + .interpretAst(this.ast, params.context || defaultInput, { + search: params.searchContext, inspectorAdapters: this.inspectorAdapters, abortSignal: this.abortController.signal, variables: params.variables, }) .then( - (v: IInterpreterResult) => { + (v: ExpressionValue) => { this.isPending = false; return v; }, () => { this.isPending = false; } - ); + ) as Promise; } cancel = () => { @@ -133,7 +131,7 @@ export class ExpressionDataHandler { } export function execute( - expression: string | ExpressionAST, + expression: string | ExpressionAstExpression, params: IExpressionLoaderParams = {} ): ExpressionDataHandler { return new ExpressionDataHandler(expression, params); diff --git a/src/plugins/expressions/public/functions/kibana_context.ts b/src/plugins/expressions/public/expression_functions/kibana_context.ts similarity index 88% rename from src/plugins/expressions/public/functions/kibana_context.ts rename to src/plugins/expressions/public/expression_functions/kibana_context.ts index 1c873573aff2d..f997972c33839 100644 --- a/src/plugins/expressions/public/functions/kibana_context.ts +++ b/src/plugins/expressions/public/expression_functions/kibana_context.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from '../../common/types'; +import { ExpressionFunctionDefinition } from '../../common'; import { KibanaContext } from '../../common/expression_types'; import { savedObjects } from '../services'; @@ -29,7 +29,7 @@ interface Arguments { savedSearchId?: string | null; } -export type ExpressionFunctionKibanaContext = ExpressionFunction< +export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition< 'kibana_context', KibanaContext | null, Arguments, @@ -39,9 +39,7 @@ export type ExpressionFunctionKibanaContext = ExpressionFunction< export const kibanaContext = (): ExpressionFunctionKibanaContext => ({ name: 'kibana_context', type: 'kibana_context', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], help: i18n.translate('expressions.functions.kibana_context.help', { defaultMessage: 'Updates kibana global context', }), @@ -76,7 +74,7 @@ export const kibanaContext = (): ExpressionFunctionKibanaContext => ({ }), }, }, - async fn(context, args, handlers) { + async fn(input, args) { const queryArg = args.q ? JSON.parse(args.q) : []; let queries = Array.isArray(queryArg) ? queryArg : [queryArg]; let filters = args.filters ? JSON.parse(args.filters) : []; @@ -89,18 +87,18 @@ export const kibanaContext = (): ExpressionFunctionKibanaContext => ({ filters = filters.concat(data.filter); } - if (context && context.query) { - queries = queries.concat(context.query); + if (input && input.query) { + queries = queries.concat(input.query); } - if (context && context.filters) { - filters = filters.concat(context.filters).filter((f: any) => !f.meta.disabled); + if (input && input.filters) { + filters = filters.concat(input.filters).filter((f: any) => !f.meta.disabled); } const timeRange = args.timeRange ? JSON.parse(args.timeRange) - : context - ? context.timeRange + : input + ? input.timeRange : undefined; return { diff --git a/src/plugins/expressions/public/fonts.ts b/src/plugins/expressions/public/fonts.ts deleted file mode 100644 index cdf3d4c16f3b5..0000000000000 --- a/src/plugins/expressions/public/fonts.ts +++ /dev/null @@ -1,151 +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 type contains a unions of all supported font labels, or the the name of - * the font the user would see in a UI. - */ -export type FontLabel = typeof fonts[number]['label']; - -/** - * This type contains a union of all supported font values, equivalent to the CSS - * `font-value` property. - */ -export type FontValue = typeof fonts[number]['value']; - -/** - * An interface representing a font in Canvas, with a textual label and the CSS - * `font-value`. - */ -export interface Font { - label: FontLabel; - value: FontValue; -} - -// This function allows one to create a strongly-typed font for inclusion in -// the font collection. As a result, the values and labels are known to the -// type system, preventing one from specifying a non-existent font at build -// time. -function createFont< - RawFont extends { value: RawFontValue; label: RawFontLabel }, - RawFontValue extends string, - RawFontLabel extends string ->(font: RawFont) { - return font; -} - -export const americanTypewriter = createFont({ - label: 'American Typewriter', - value: "'American Typewriter', 'Courier New', Courier, Monaco, mono", -}); - -export const arial = createFont({ label: 'Arial', value: 'Arial, sans-serif' }); - -export const baskerville = createFont({ - label: 'Baskerville', - value: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif", -}); - -export const bookAntiqua = createFont({ - label: 'Book Antiqua', - value: "'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif", -}); - -export const brushScript = createFont({ - label: 'Brush Script', - value: "'Brush Script MT', 'Comic Sans', sans-serif", -}); - -export const chalkboard = createFont({ - label: 'Chalkboard', - value: "Chalkboard, 'Comic Sans', sans-serif", -}); - -export const didot = createFont({ - label: 'Didot', - value: "Didot, Georgia, Garamond, 'Times New Roman', Times, serif", -}); - -export const futura = createFont({ - label: 'Futura', - value: 'Futura, Impact, Helvetica, Arial, sans-serif', -}); - -export const gillSans = createFont({ - label: 'Gill Sans', - value: - "'Gill Sans', 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif", -}); - -export const helveticaNeue = createFont({ - label: 'Helvetica Neue', - value: "'Helvetica Neue', Helvetica, Arial, sans-serif", -}); - -export const hoeflerText = createFont({ - label: 'Hoefler Text', - value: "'Hoefler Text', Garamond, Georgia, 'Times New Roman', Times, serif", -}); - -export const lucidaGrande = createFont({ - label: 'Lucida Grande', - value: "'Lucida Grande', 'Lucida Sans Unicode', Lucida, Verdana, Helvetica, Arial, sans-serif", -}); - -export const myriad = createFont({ - label: 'Myriad', - value: 'Myriad, Helvetica, Arial, sans-serif', -}); - -export const openSans = createFont({ - label: 'Open Sans', - value: "'Open Sans', Helvetica, Arial, sans-serif", -}); - -export const optima = createFont({ - label: 'Optima', - value: "Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif", -}); - -export const palatino = createFont({ - label: 'Palatino', - value: "Palatino, 'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif", -}); - -/** - * A collection of supported fonts. - */ -export const fonts = [ - americanTypewriter, - arial, - baskerville, - bookAntiqua, - brushScript, - chalkboard, - didot, - futura, - gillSans, - helveticaNeue, - hoeflerText, - lucidaGrande, - myriad, - openSans, - optima, - palatino, -]; diff --git a/src/plugins/expressions/public/functions/font.ts b/src/plugins/expressions/public/functions/font.ts deleted file mode 100644 index 096f0ef196be3..0000000000000 --- a/src/plugins/expressions/public/functions/font.ts +++ /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 { i18n } from '@kbn/i18n'; -import { openSans, FontLabel as FontFamily } from '../fonts'; -import { - ExpressionFunction, - CSSStyle, - FontStyle, - FontWeight, - Style, - TextAlignment, - TextDecoration, -} from '../../common/types'; - -const dashify = (str: string) => { - return str - .trim() - .replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/\W/g, m => (/[À-ž]/.test(m) ? m : '-')) - .replace(/^-+|-+$/g, '') - .toLowerCase(); -}; - -const inlineStyle = (obj: Record) => { - if (!obj) return ''; - const styles = Object.keys(obj).map(key => { - const prop = dashify(key); - const line = prop.concat(':').concat(String(obj[key])); - return line; - }); - return styles.join(';'); -}; - -interface Arguments { - align?: TextAlignment; - color?: string; - family?: FontFamily; - italic?: boolean; - lHeight?: number | null; - size?: number; - underline?: boolean; - weight?: FontWeight; -} - -export function font(): ExpressionFunction<'font', null, Arguments, Style> { - return { - name: 'font', - aliases: [], - type: 'style', - help: i18n.translate('expressions.functions.fontHelpText', { - defaultMessage: 'Create a font style.', - }), - context: { - types: ['null'], - }, - args: { - align: { - default: 'left', - help: i18n.translate('expressions.functions.font.args.alignHelpText', { - defaultMessage: 'The horizontal text alignment.', - }), - options: Object.values(TextAlignment), - types: ['string'], - }, - color: { - help: i18n.translate('expressions.functions.font.args.colorHelpText', { - defaultMessage: 'The text color.', - }), - types: ['string'], - }, - family: { - default: `"${openSans.value}"`, - help: i18n.translate('expressions.functions.font.args.familyHelpText', { - defaultMessage: 'An acceptable {css} web font string', - values: { - css: 'CSS', - }, - }), - types: ['string'], - }, - italic: { - default: false, - help: i18n.translate('expressions.functions.font.args.italicHelpText', { - defaultMessage: 'Italicize the text?', - }), - options: [true, false], - types: ['boolean'], - }, - lHeight: { - default: null, - aliases: ['lineHeight'], - help: i18n.translate('expressions.functions.font.args.lHeightHelpText', { - defaultMessage: 'The line height in pixels', - }), - types: ['number', 'null'], - }, - size: { - default: 14, - help: i18n.translate('expressions.functions.font.args.sizeHelpText', { - defaultMessage: 'The font size in pixels', - }), - types: ['number'], - }, - underline: { - default: false, - help: i18n.translate('expressions.functions.font.args.underlineHelpText', { - defaultMessage: 'Underline the text?', - }), - options: [true, false], - types: ['boolean'], - }, - weight: { - default: 'normal', - help: i18n.translate('expressions.functions.font.args.weightHelpText', { - defaultMessage: 'The font weight. For example, {list}, or {end}.', - values: { - list: Object.values(FontWeight) - .slice(0, -1) - .map(weight => `\`"${weight}"\``) - .join(', '), - end: `\`"${Object.values(FontWeight).slice(-1)[0]}"\``, - }, - }), - options: Object.values(FontWeight), - types: ['string'], - }, - }, - fn: (_context, args) => { - if (!Object.values(FontWeight).includes(args.weight!)) { - throw new Error( - i18n.translate('expressions.functions.font.invalidFontWeightErrorMessage', { - defaultMessage: "Invalid font weight: '{weight}'", - values: { - weight: args.weight, - }, - }) - ); - } - if (!Object.values(TextAlignment).includes(args.align!)) { - throw new Error( - i18n.translate('expressions.functions.font.invalidTextAlignmentErrorMessage', { - defaultMessage: "Invalid text alignment: '{align}'", - values: { - align: args.align, - }, - }) - ); - } - - // the line height shouldn't ever be lower than the size, and apply as a - // pixel setting - const lineHeight = args.lHeight != null ? `${args.lHeight}px` : '1'; - - const spec: CSSStyle = { - fontFamily: args.family, - fontWeight: args.weight, - fontStyle: args.italic ? FontStyle.ITALIC : FontStyle.NORMAL, - textDecoration: args.underline ? TextDecoration.UNDERLINE : TextDecoration.NONE, - textAlign: args.align, - fontSize: `${args.size}px`, // apply font size as a pixel setting - lineHeight, // apply line height as a pixel setting - }; - - // conditionally apply styles based on input - if (args.color) { - spec.color = args.color; - } - - return { - type: 'style', - spec, - css: inlineStyle(spec as Record), - }; - }, - }; -} diff --git a/src/plugins/expressions/public/functions/tests/var.test.ts b/src/plugins/expressions/public/functions/tests/var.test.ts deleted file mode 100644 index fe5963ec8c509..0000000000000 --- a/src/plugins/expressions/public/functions/tests/var.test.ts +++ /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 { functionWrapper } from './utils'; -import { variable } from '../var'; -import { FunctionHandlers } from '../../../common/types'; -import { KibanaContext } from '../../../common/expression_types/kibana_context'; - -describe('interpreter/functions#var', () => { - const fn = functionWrapper(variable); - let context: Partial; - let initialContext: KibanaContext; - let handlers: FunctionHandlers; - - beforeEach(() => { - context = { timeRange: { from: '0', to: '1' } }; - initialContext = { - type: 'kibana_context', - query: { language: 'lucene', query: 'geo.src:US' }, - filters: [ - { - meta: { - disabled: false, - negate: false, - alias: null, - }, - query: { match: {} }, - }, - ], - timeRange: { from: '2', to: '3' }, - }; - handlers = { - getInitialContext: () => initialContext, - variables: { test: 1 } as any, - }; - }); - - it('returns the selected variable', () => { - const actual = fn(context, { name: 'test' }, handlers); - expect(actual).toEqual(1); - }); - - it('returns undefined if variable does not exist', () => { - const actual = fn(context, { name: 'unknown' }, handlers); - expect(actual).toEqual(undefined); - }); -}); diff --git a/src/plugins/expressions/public/functions/tests/var_set.test.ts b/src/plugins/expressions/public/functions/tests/var_set.test.ts deleted file mode 100644 index 7efa8ebc0dd3f..0000000000000 --- a/src/plugins/expressions/public/functions/tests/var_set.test.ts +++ /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 { functionWrapper } from './utils'; -import { variableSet } from '../var_set'; -import { FunctionHandlers } from '../../../common/types'; -import { KibanaContext } from '../../../common/expression_types/kibana_context'; - -describe('interpreter/functions#varset', () => { - const fn = functionWrapper(variableSet); - let context: Partial; - let initialContext: KibanaContext; - let handlers: FunctionHandlers; - let variables: Record; - - beforeEach(() => { - context = { timeRange: { from: '0', to: '1' } }; - initialContext = { - type: 'kibana_context', - query: { language: 'lucene', query: 'geo.src:US' }, - filters: [ - { - meta: { - disabled: false, - negate: false, - alias: null, - }, - query: { match: {} }, - }, - ], - timeRange: { from: '2', to: '3' }, - }; - handlers = { - getInitialContext: () => initialContext, - variables: { test: 1 } as any, - }; - - variables = handlers.variables; - }); - - it('updates a variable', () => { - const actual = fn(context, { name: 'test', value: 2 }, handlers); - expect(variables.test).toEqual(2); - expect(actual).toEqual(context); - }); - - it('sets a new variable', () => { - const actual = fn(context, { name: 'new', value: 3 }, handlers); - expect(variables.new).toEqual(3); - expect(actual).toEqual(context); - }); - - it('stores context if value is not set', () => { - const actual = fn(context, { name: 'test' }, handlers); - expect(variables.test).toEqual(context); - expect(actual).toEqual(context); - }); -}); diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 951d643c9df68..59d529dc9caff 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -20,17 +20,97 @@ import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; +// Kibana Platform. export { ExpressionsPublicPlugin as Plugin }; - export * from './plugin'; -export * from './types'; -export * from '../common'; -export { interpreterProvider, ExpressionInterpret } from './interpreter_provider'; -export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; -export { ExpressionDataHandler } from './execute'; - -export { ExpressionRenderHandler } from './render'; - export function plugin(initializerContext: PluginInitializerContext) { return new ExpressionsPublicPlugin(initializerContext); } + +// Static exports. +export { ExpressionExecutor, IExpressionLoaderParams } from './types'; +export { + ExpressionRendererComponent, + ReactExpressionRenderer, + ReactExpressionRendererProps, + ReactExpressionRendererType, +} from './react_expression_renderer'; +export { ExpressionDataHandler } from './execute'; +export { ExpressionRenderHandler } from './render'; +export { + AnyExpressionFunctionDefinition, + AnyExpressionTypeDefinition, + ArgumentType, + Datatable, + DatatableColumn, + DatatableColumnType, + DatatableRow, + Execution, + ExecutionContainer, + ExecutionContext, + ExecutionParams, + ExecutionState, + Executor, + ExecutorContainer, + ExecutorState, + ExpressionAstArgument, + ExpressionAstExpression, + ExpressionAstFunction, + ExpressionAstNode, + ExpressionFunction, + ExpressionFunctionDefinition, + ExpressionFunctionKibana, + ExpressionFunctionParameter, + ExpressionImage, + ExpressionRenderDefinition, + ExpressionRenderer, + ExpressionRendererRegistry, + ExpressionType, + ExpressionTypeDefinition, + ExpressionTypeStyle, + ExpressionValue, + ExpressionValueBoxed, + ExpressionValueConverter, + ExpressionValueError, + ExpressionValueNum, + ExpressionValueRender, + ExpressionValueSearchContext, + ExpressionValueUnboxed, + Filter, + Font, + FontLabel, + FontStyle, + FontValue, + FontWeight, + format, + formatExpression, + FunctionsRegistry, + IInterpreterRenderHandlers, + InterpreterErrorType, + IRegistry, + KIBANA_CONTEXT_NAME, + KibanaContext, + KibanaDatatable, + KibanaDatatableColumn, + KibanaDatatableRow, + KnownTypeToString, + Overflow, + parse, + parseExpression, + PointSeries, + PointSeriesColumn, + PointSeriesColumnName, + PointSeriesColumns, + PointSeriesRow, + Range, + SerializedDatatable, + SerializedFieldFormat, + Style, + TextAlignment, + TextDecoration, + TypesRegistry, + TypeString, + TypeToString, + UnmappedTypeStrings, + ExpressionValueRender as Render, +} from '../common'; diff --git a/src/plugins/expressions/public/interpreter_provider.ts b/src/plugins/expressions/public/interpreter_provider.ts deleted file mode 100644 index f4b65c630089a..0000000000000 --- a/src/plugins/expressions/public/interpreter_provider.ts +++ /dev/null @@ -1,253 +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 { fromExpression, getByAlias } from '@kbn/interpreter/common'; - -import { clone, each, keys, last, mapValues, reduce, zipObject } from 'lodash'; -import { createError } from './create_error'; -import { - ExpressionAST, - ExpressionFunctionAST, - AnyExpressionFunction, - ArgumentType, -} from '../common/types'; -import { getType } from '../common/type'; -import { FunctionsRegistry } from './registries'; - -export { createError }; - -export interface InterpreterConfig { - functions: FunctionsRegistry; - types: any; - handlers: any; -} - -export type ExpressionInterpret = (ast: ExpressionAST, context?: any) => any; - -export function interpreterProvider(config: InterpreterConfig): ExpressionInterpret { - const { functions, types } = config; - const handlers = { ...config.handlers, types }; - - function cast(node: any, toTypeNames: any) { - // If you don't give us anything to cast to, you'll get your input back - if (!toTypeNames || toTypeNames.length === 0) return node; - - // No need to cast if node is already one of the valid types - const fromTypeName = getType(node); - if (toTypeNames.includes(fromTypeName)) return node; - - const fromTypeDef = types[fromTypeName]; - - for (let i = 0; i < toTypeNames.length; i++) { - // First check if the current type can cast to this type - if (fromTypeDef && fromTypeDef.castsTo(toTypeNames[i])) { - return fromTypeDef.to(node, toTypeNames[i], types); - } - - // If that isn't possible, check if this type can cast from the current type - const toTypeDef = types[toTypeNames[i]]; - if (toTypeDef && toTypeDef.castsFrom(fromTypeName)) return toTypeDef.from(node, types); - } - - throw new Error(`Can not cast '${fromTypeName}' to any of '${toTypeNames.join(', ')}'`); - } - - async function invokeChain(chainArr: ExpressionFunctionAST[], context: any): Promise { - if (!chainArr.length) return context; - // if execution was aborted return error - if (handlers.abortSignal && handlers.abortSignal.aborted) { - return createError({ - message: 'The expression was aborted.', - name: 'AbortError', - }); - } - const chain = clone(chainArr); - const link = chain.shift(); // Every thing in the chain will always be a function right? - if (!link) throw Error('Function chain is empty.'); - const { function: fnName, arguments: fnArgs } = link; - const fnDef = getByAlias(functions.toJS(), fnName); - - if (!fnDef) { - return createError({ message: `Function ${fnName} could not be found.` }); - } - - try { - // Resolve arguments before passing to function - // resolveArgs returns an object because the arguments themselves might - // actually have a 'then' function which would be treated as a promise - const { resolvedArgs } = await resolveArgs(fnDef, context, fnArgs); - const newContext = await invokeFunction(fnDef, context, resolvedArgs); - - // if something failed, just return the failure - if (getType(newContext) === 'error') return newContext; - - // Continue re-invoking chain until it's empty - return invokeChain(chain, newContext); - } catch (e) { - // Everything that throws from a function will hit this - // The interpreter should *never* fail. It should always return a `{type: error}` on failure - e.message = `[${fnName}] > ${e.message}`; - return createError(e); - } - } - - async function invokeFunction( - fnDef: AnyExpressionFunction, - context: any, - args: Record - ): Promise { - // Check function input. - const acceptableContext = cast(context, fnDef.context ? fnDef.context.types : undefined); - const fnOutput = await fnDef.fn(acceptableContext, args, handlers); - - // Validate that the function returned the type it said it would. - // This isn't really required, but it keeps function developers honest. - const returnType = getType(fnOutput); - const expectedType = fnDef.type; - if (expectedType && returnType !== expectedType) { - throw new Error( - `Function '${fnDef.name}' should return '${expectedType}',` + - ` actually returned '${returnType}'` - ); - } - - // Validate the function output against the type definition's validate function - const type = handlers.types[fnDef.type]; - if (type && type.validate) { - try { - type.validate(fnOutput); - } catch (e) { - throw new Error(`Output of '${fnDef.name}' is not a valid type '${fnDef.type}': ${e}`); - } - } - - return fnOutput; - } - - // Processes the multi-valued AST argument values into arguments that can be passed to the function - async function resolveArgs( - fnDef: AnyExpressionFunction, - context: any, - argAsts: any - ): Promise { - const argDefs = fnDef.args; - - // Use the non-alias name from the argument definition - const dealiasedArgAsts = reduce( - argAsts, - (acc, argAst, argName) => { - const argDef = getByAlias(argDefs, argName); - // TODO: Implement a system to allow for undeclared arguments - if (!argDef) { - throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`); - } - - acc[argDef.name] = (acc[argDef.name] || []).concat(argAst); - return acc; - }, - {} as any - ); - - // Check for missing required arguments - each(argDefs, argDef => { - const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType< - any - > & { name: string }; - if ( - typeof argDefault === 'undefined' && - required && - typeof dealiasedArgAsts[argName] === 'undefined' - ) { - if (!aliases || aliases.length === 0) { - throw new Error(`${fnDef.name} requires an argument`); - } else { - const errorArg = argName === '_' ? aliases[0] : argName; // use an alias if _ is the missing arg - throw new Error(`${fnDef.name} requires an "${errorArg}" argument`); - } - } - }); - - // Fill in default values from argument definition - const argAstsWithDefaults = reduce( - argDefs, - (acc: any, argDef: any, argName: any) => { - if (typeof acc[argName] === 'undefined' && typeof argDef.default !== 'undefined') { - acc[argName] = [(fromExpression as any)(argDef.default, 'argument')]; - } - - return acc; - }, - dealiasedArgAsts - ); - - // Create the functions to resolve the argument ASTs into values - // These are what are passed to the actual functions if you opt out of resolving - const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { - return asts.map((item: any) => { - return async (ctx = context) => { - const newContext = await interpret(item, ctx); - // This is why when any sub-expression errors, the entire thing errors - if (getType(newContext) === 'error') throw newContext.error; - return cast(newContext, argDefs[argName as any].types); - }; - }); - }); - - const argNames = keys(resolveArgFns); - - // Actually resolve unless the argument definition says not to - const resolvedArgValues = await Promise.all( - argNames.map(argName => { - const interpretFns = resolveArgFns[argName]; - if (!argDefs[argName].resolve) return interpretFns; - return Promise.all(interpretFns.map((fn: any) => fn())); - }) - ); - - const resolvedMultiArgs = zipObject(argNames, resolvedArgValues); - - // Just return the last unless the argument definition allows multiple - const resolvedArgs = mapValues(resolvedMultiArgs, (argValues, argName) => { - if (argDefs[argName as any].multi) return argValues; - return last(argValues as any); - }); - - // Return an object here because the arguments themselves might actually have a 'then' - // function which would be treated as a promise - return { resolvedArgs }; - } - - const interpret: ExpressionInterpret = async function interpret(ast, context = null) { - const type = getType(ast); - switch (type) { - case 'expression': - return invokeChain(ast.chain, context); - case 'string': - case 'number': - case 'null': - case 'boolean': - return ast; - default: - throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`); - } - }; - - return interpret; -} diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 0a01cc29ff9dc..480434244d6f5 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -18,12 +18,10 @@ */ import { first, skip, toArray } from 'rxjs/operators'; -import { fromExpression } from '@kbn/interpreter/common'; import { loader, ExpressionLoader } from './loader'; import { ExpressionDataHandler } from './execute'; -import { IInterpreterRenderHandlers } from './types'; import { Observable } from 'rxjs'; -import { ExpressionAST } from '../common/types'; +import { ExpressionAstExpression, parseExpression, IInterpreterRenderHandlers } from '../common'; const element: HTMLElement = null as any; @@ -38,7 +36,7 @@ jest.mock('./services', () => { return { getInterpreter: () => { return { - interpretAst: async (expression: ExpressionAST) => { + interpretAst: async (expression: ExpressionAstExpression) => { return { type: 'render', as: 'test' }; }, }; @@ -83,7 +81,7 @@ describe('ExpressionLoader', () => { }); it('accepts expression AST', () => { - const expressionAST = fromExpression(expressionString) as ExpressionAST; + const expressionAST = parseExpression(expressionString); const expressionLoader = new ExpressionLoader(element, expressionAST, {}); expect(expressionLoader.getExpression()).toEqual(expressionString); expect(expressionLoader.getAst()).toEqual(expressionAST); diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index d714282360f71..320a8469fe9e3 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -22,10 +22,12 @@ import { filter, map } from 'rxjs/operators'; import { Adapters, InspectorSession } from '../../inspector/public'; import { ExpressionDataHandler } from './execute'; import { ExpressionRenderHandler } from './render'; -import { Data, IExpressionLoaderParams } from './types'; -import { ExpressionAST } from '../common/types'; +import { IExpressionLoaderParams } from './types'; +import { ExpressionAstExpression } from '../common'; import { getInspector } from './services'; +type Data = any; + export class ExpressionLoader { data$: Observable; update$: ExpressionRenderHandler['update$']; @@ -42,7 +44,7 @@ export class ExpressionLoader { constructor( element: HTMLElement, - expression?: string | ExpressionAST, + expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams ) { this.dataSubject = new Subject(); @@ -64,8 +66,11 @@ export class ExpressionLoader { this.update$ = this.renderHandler.update$; this.events$ = this.renderHandler.events$; - this.update$.subscribe(({ newExpression, newParams }) => { - this.update(newExpression, newParams); + this.update$.subscribe(value => { + if (value) { + const { newExpression, newParams } = value; + this.update(newExpression, newParams); + } }); this.data$.subscribe(data => { @@ -105,7 +110,7 @@ export class ExpressionLoader { } } - getAst(): ExpressionAST | undefined { + getAst(): ExpressionAstExpression | undefined { if (this.dataHandler) { return this.dataHandler.getAst(); } @@ -130,7 +135,7 @@ export class ExpressionLoader { } } - update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): void { + update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void { this.setParams(params); this.loadingSubject.next(true); @@ -142,7 +147,7 @@ export class ExpressionLoader { } private loadData = async ( - expression: string | ExpressionAST, + expression: string | ExpressionAstExpression, params: IExpressionLoaderParams ): Promise => { if (this.dataHandler && this.dataHandler.isPending) { @@ -186,7 +191,7 @@ export class ExpressionLoader { export type IExpressionLoader = ( element: HTMLElement, - expression: string | ExpressionAST, + expression: string | ExpressionAstExpression, params: IExpressionLoaderParams ) => ExpressionLoader; diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index a3476a24dd7ed..70760ada83955 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -31,9 +31,16 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + getFunction: jest.fn(), + getFunctions: jest.fn(), + getRenderer: jest.fn(), + getRenderers: jest.fn(), + getType: jest.fn(), + getTypes: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), + run: jest.fn(), __LEGACY: { functions: { register: () => {}, @@ -46,7 +53,7 @@ const createSetupContract = (): Setup => { } as any, getExecutor: () => ({ interpreter: { - interpretAst: () => {}, + interpretAst: (() => {}) as any, }, }), loadLegacyServerFunctionWrappers: () => Promise.resolve(), @@ -60,10 +67,17 @@ const createStartContract = (): Start => { execute: jest.fn(), ExpressionDataHandler: jest.fn(), ExpressionLoader: jest.fn(), - ExpressionRenderer: jest.fn(props => <>), ExpressionRenderHandler: jest.fn(), + getFunction: jest.fn(), + getFunctions: jest.fn(), + getRenderer: jest.fn(), + getRenderers: jest.fn(), + getType: jest.fn(), + getTypes: jest.fn(), loader: jest.fn(), + ReactExpressionRenderer: jest.fn(props => <>), render: jest.fn(), + run: jest.fn(), }; }; diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts new file mode 100644 index 0000000000000..5437a7d21f338 --- /dev/null +++ b/src/plugins/expressions/public/plugin.test.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { expressionsPluginMock } from './mocks'; +import { add } from '../common/test_helpers/expression_functions/add'; + +describe('ExpressionsPublicPlugin', () => { + test('can instantiate from mocks', async () => { + const { setup } = await expressionsPluginMock.createPlugin(); + expect(typeof setup.registerFunction).toBe('function'); + }); + + describe('setup contract', () => { + describe('.registerFunction()', () => { + test('can register a function', async () => { + const { setup } = await expressionsPluginMock.createPlugin(); + expect(setup.getFunctions().add).toBe(undefined); + setup.registerFunction(add); + expect(setup.getFunctions().add.name).toBe('add'); + }); + }); + + describe('.run()', () => { + test('can execute simple expression', async () => { + const { setup } = await expressionsPluginMock.createPlugin(); + const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null); + expect(bar).toBe('bar'); + }); + }); + }); + + describe('start contract', () => { + describe('.execute()', () => { + test('can parse a single function expression', async () => { + const { doStart } = await expressionsPluginMock.createPlugin(); + const start = await doStart(); + + const handler = start.execute('clog'); + expect(handler.getAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "clog", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + }); + }); +}); diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 034be58ec9e35..6799b1590f252 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -18,10 +18,18 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { ExpressionInterpretWithHandlers, ExpressionExecutor } from './types'; -import { FunctionsRegistry, RenderFunctionsRegistry, TypesRegistry } from './registries'; -import { BfetchPublicSetup, BfetchPublicStart } from '../../bfetch/public'; +import { ExpressionExecutor } from './types'; +import { + ExpressionRendererRegistry, + FunctionsRegistry, + serializeProvider, + TypesRegistry, + ExpressionsService, + ExpressionsServiceSetup, + ExpressionsServiceStart, +} from '../common'; import { Setup as InspectorSetup, Start as InspectorStart } from '../../inspector/public'; +import { BfetchPublicSetup, BfetchPublicStart } from '../../bfetch/public'; import { setCoreStart, setInspector, @@ -29,37 +37,11 @@ import { setRenderersRegistry, setNotifications, } from './services'; -import { clog as clogFunction } from './functions/clog'; -import { font as fontFunction } from './functions/font'; -import { kibana as kibanaFunction } from './functions/kibana'; -import { kibanaContext as kibanaContextFunction } from './functions/kibana_context'; -import { variable } from './functions/var'; -import { variableSet } from './functions/var_set'; -import { - boolean as booleanType, - datatable as datatableType, - error as errorType, - filter as filterType, - image as imageType, - nullType, - number as numberType, - pointseries, - range as rangeType, - render as renderType, - shape as shapeType, - string as stringType, - style as styleType, - kibanaContext as kibanaContextType, - kibanaDatatable as kibanaDatatableType, -} from '../common/expression_types'; -import { interpreterProvider } from './interpreter_provider'; -import { createHandlers } from './create_handlers'; -import { ExpressionRendererImplementation } from './expression_renderer'; +import { kibanaContext as kibanaContextFunction } from './expression_functions/kibana_context'; +import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader, loader } from './loader'; import { ExpressionDataHandler, execute } from './execute'; import { render, ExpressionRenderHandler } from './render'; -import { AnyExpressionFunction, AnyExpressionType } from '../common/types'; -import { serializeProvider } from '../common'; export interface ExpressionsSetupDeps { bfetch: BfetchPublicSetup; @@ -71,82 +53,77 @@ export interface ExpressionsStartDeps { inspector: InspectorStart; } -export interface ExpressionsSetup { - registerFunction: (fn: AnyExpressionFunction | (() => AnyExpressionFunction)) => void; - registerRenderer: (renderer: any) => void; - registerType: (type: () => AnyExpressionType) => void; +export interface ExpressionsSetup extends ExpressionsServiceSetup { + /** + * @todo Get rid of these `__LEGACY` APIs. + * + * `__LEGACY` APIs are used by Canvas. It should be possible to stop + * using all of them (except `loadLegacyServerFunctionWrappers`) and use + * Kibana Platform plugin contracts instead. + */ __LEGACY: { - functions: FunctionsRegistry; - renderers: RenderFunctionsRegistry; + /** + * Use `registerType` and `getTypes` instead. + */ types: TypesRegistry; + + /** + * Use `registerFunction` and `getFunctions` instead. + */ + functions: FunctionsRegistry; + + /** + * Use `registerRenderer` and `getRenderers`, and `getRenderer` instead. + */ + renderers: ExpressionRendererRegistry; + + /** + * Use `run` function instead. + */ getExecutor: () => ExpressionExecutor; + + /** + * This function is used by Canvas to load server-side function and create + * browser-side "wrapper" for each one. This function can be removed once + * we enable expressions on server-side: https://github.com/elastic/kibana/issues/46906 + */ loadLegacyServerFunctionWrappers: () => Promise; }; } -export interface ExpressionsStart { +export interface ExpressionsStart extends ExpressionsServiceStart { execute: typeof execute; ExpressionDataHandler: typeof ExpressionDataHandler; ExpressionLoader: typeof ExpressionLoader; - ExpressionRenderer: typeof ExpressionRendererImplementation; ExpressionRenderHandler: typeof ExpressionRenderHandler; loader: typeof loader; + ReactExpressionRenderer: typeof ReactExpressionRenderer; render: typeof render; } export class ExpressionsPublicPlugin implements Plugin { - private readonly functions = new FunctionsRegistry(); - private readonly renderers = new RenderFunctionsRegistry(); - private readonly types = new TypesRegistry(); + private readonly expressions: ExpressionsService = new ExpressionsService(); constructor(initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { inspector, bfetch }: ExpressionsSetupDeps): ExpressionsSetup { - const { functions, renderers, types } = this; + const { expressions } = this; + const { executor, renderers } = expressions; + + executor.extendContext({ + environment: 'client', + }); + executor.registerFunction(kibanaContextFunction()); setRenderersRegistry(renderers); - const registerFunction: ExpressionsSetup['registerFunction'] = fn => { - functions.register(fn); - }; + const expressionsSetup = expressions.setup(); - registerFunction(clogFunction); - registerFunction(fontFunction); - registerFunction(kibanaFunction); - registerFunction(kibanaContextFunction); - registerFunction(variable); - registerFunction(variableSet); - - types.register(booleanType); - types.register(datatableType); - types.register(errorType); - types.register(filterType); - types.register(imageType); - types.register(nullType); - types.register(numberType); - types.register(pointseries); - types.register(rangeType); - types.register(renderType); - types.register(shapeType); - types.register(stringType); - types.register(styleType); - types.register(kibanaContextType); - types.register(kibanaDatatableType); - - // TODO: Refactor this function. - const getExecutor = () => { - const interpretAst: ExpressionInterpretWithHandlers = (ast, context, handlers) => { - const interpret = interpreterProvider({ - types: types.toJS(), - handlers: { ...handlers, ...createHandlers() }, - functions, - }); - return interpret(ast, context); - }; - const executor: ExpressionExecutor = { interpreter: { interpretAst } }; - return executor; + // This is legacy. Should go away when we get rid of __LEGACY. + const getExecutor = (): ExpressionExecutor => { + return { interpreter: { interpretAst: expressionsSetup.run } }; }; setInterpreter(getExecutor().interpreter); @@ -157,22 +134,22 @@ export class ExpressionsPublicPlugin cached = (async () => { const serverFunctionList = await core.http.get(`/api/interpreter/fns`); const batchedFunction = bfetch.batchedFunction({ url: `/api/interpreter/fns` }); - const { serialize } = serializeProvider(types.toJS()); + const { serialize } = serializeProvider(executor.getTypes()); // For every sever-side function, register a client-side // function that matches its definition, but which simply // calls the server-side function endpoint. Object.keys(serverFunctionList).forEach(functionName => { - if (functions.get(functionName)) { + if (expressionsSetup.getFunction(functionName)) { return; } const fn = () => ({ ...serverFunctionList[functionName], - fn: (context: any, args: any) => { - return batchedFunction({ functionName, args, context: serialize(context) }); + fn: (input: any, args: any) => { + return batchedFunction({ functionName, args, context: serialize(input) }); }, }); - registerFunction(fn); + expressionsSetup.registerFunction(fn); }); })(); } @@ -180,17 +157,11 @@ export class ExpressionsPublicPlugin }; const setup: ExpressionsSetup = { - registerFunction, - registerRenderer: (renderer: any) => { - renderers.register(renderer); - }, - registerType: type => { - types.register(type); - }, + ...expressionsSetup, __LEGACY: { - functions, + types: executor.types, + functions: executor.functions, renderers, - types, getExecutor, loadLegacyServerFunctionWrappers, }, @@ -199,18 +170,22 @@ export class ExpressionsPublicPlugin return setup; } - public start(core: CoreStart, { inspector }: ExpressionsStartDeps): ExpressionsStart { + public start(core: CoreStart, { inspector, bfetch }: ExpressionsStartDeps): ExpressionsStart { setCoreStart(core); setInspector(inspector); setNotifications(core.notifications); + const { expressions } = this; + const expressionsStart = expressions.start(); + return { + ...expressionsStart, execute, ExpressionDataHandler, ExpressionLoader, - ExpressionRenderer: ExpressionRendererImplementation, ExpressionRenderHandler, loader, + ReactExpressionRenderer, render, }; } diff --git a/src/plugins/expressions/public/expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx similarity index 95% rename from src/plugins/expressions/public/expression_renderer.test.tsx rename to src/plugins/expressions/public/react_expression_renderer.test.tsx index 217618bc3a177..65cc5fc1569cb 100644 --- a/src/plugins/expressions/public/expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { Subject } from 'rxjs'; import { share } from 'rxjs/operators'; -import { ExpressionRendererImplementation } from './expression_renderer'; +import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader } from './loader'; import { mount } from 'enzyme'; import { EuiProgress } from '@elastic/eui'; @@ -54,7 +54,7 @@ describe('ExpressionRenderer', () => { }; }); - const instance = mount(); + const instance = mount(); act(() => { loadingSubject.next(); @@ -108,7 +108,7 @@ describe('ExpressionRenderer', () => { }); const instance = mount( -
{message}
} /> diff --git a/src/plugins/expressions/public/expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx similarity index 91% rename from src/plugins/expressions/public/expression_renderer.tsx rename to src/plugins/expressions/public/react_expression_renderer.tsx index 5c04d8405479f..242a49c6d6639 100644 --- a/src/plugins/expressions/public/expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -25,27 +25,29 @@ import { filter } from 'rxjs/operators'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { IExpressionLoaderParams, IInterpreterRenderHandlers, RenderError } from './types'; -import { ExpressionAST } from '../common/types'; +import { IExpressionLoaderParams, RenderError } from './types'; +import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common'; import { ExpressionLoader } from './loader'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself -export interface ExpressionRendererProps extends IExpressionLoaderParams { +export interface ReactExpressionRendererProps extends IExpressionLoaderParams { className?: string; dataAttrs?: string[]; - expression: string | ExpressionAST; + expression: string | ExpressionAstExpression; renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; } +export type ReactExpressionRendererType = React.ComponentType; + interface State { isEmpty: boolean; isLoading: boolean; error: null | RenderError; } -export type ExpressionRenderer = React.FC; +export type ExpressionRendererComponent = React.FC; const defaultState: State = { isEmpty: true, @@ -53,14 +55,14 @@ const defaultState: State = { error: null, }; -export const ExpressionRendererImplementation = ({ +export const ReactExpressionRenderer = ({ className, dataAttrs, padding, renderError, expression, ...expressionLoaderOptions -}: ExpressionRendererProps) => { +}: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); const [state, setState] = useState({ ...defaultState }); const hasCustomRenderErrorHandler = !!renderError; diff --git a/src/plugins/expressions/public/registries/function_registry.ts b/src/plugins/expressions/public/registries/function_registry.ts deleted file mode 100644 index 43d6086274fc0..0000000000000 --- a/src/plugins/expressions/public/registries/function_registry.ts +++ /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. - */ - -/* eslint-disable max-classes-per-file */ - -import { - ArgumentType, - ExpressionValue, - AnyExpressionFunction, - FunctionHandlers, -} from '../../common/types'; -import { Registry } from './registry'; - -export class FunctionParameter { - name: string; - required: boolean; - help: string; - types: string[]; - default: any; - aliases: string[]; - multi: boolean; - resolve: boolean; - options: any[]; - - constructor(name: string, arg: ArgumentType) { - const { required, help, types, aliases, multi, resolve, options } = arg; - - if (name === '_') { - throw Error('Arg names must not be _. Use it in aliases instead.'); - } - - this.name = name; - this.required = !!required; - this.help = help || ''; - this.types = types || []; - this.default = arg.default; - this.aliases = aliases || []; - this.multi = !!multi; - this.resolve = resolve == null ? true : resolve; - this.options = options || []; - } - - accepts(type: string) { - if (!this.types.length) return true; - return this.types.indexOf(type) > -1; - } -} - -export class Function { - /** - * Name of function - */ - name: string; - - /** - * Aliases that can be used instead of `name`. - */ - aliases: string[]; - - /** - * Return type of function. This SHOULD be supplied. We use it for UI - * and autocomplete hinting. We may also use it for optimizations in - * the future. - */ - type: string; - - /** - * Function to run function (context, args) - */ - fn: ( - input: ExpressionValue, - params: Record, - handlers: FunctionHandlers - ) => ExpressionValue; - - /** - * A short help text. - */ - help: string; - - args: Record = {}; - - context: { types?: string[] }; - - constructor(functionDefinition: AnyExpressionFunction) { - const { name, type, aliases, fn, help, args, context } = functionDefinition; - - this.name = name; - this.type = type; - this.aliases = aliases || []; - this.fn = (input, params, handlers) => Promise.resolve(fn(input, params, handlers)); - this.help = help || ''; - this.context = context || {}; - - for (const [key, arg] of Object.entries(args || {})) { - this.args[key] = new FunctionParameter(key, arg); - } - } - - accepts = (type: string): boolean => { - // If you don't tell us about context, we'll assume you don't care what you get. - if (!this.context.types) return true; - return this.context.types.indexOf(type) > -1; - }; -} - -export class FunctionsRegistry extends Registry { - register(functionDefinition: AnyExpressionFunction | (() => AnyExpressionFunction)) { - const fn = new Function( - typeof functionDefinition === 'object' ? functionDefinition : functionDefinition() - ); - this.set(fn.name, fn); - } -} diff --git a/src/plugins/expressions/public/registries/render_registry.ts b/src/plugins/expressions/public/registries/render_registry.ts deleted file mode 100644 index 6fd48f5f0c6af..0000000000000 --- a/src/plugins/expressions/public/registries/render_registry.ts +++ /dev/null @@ -1,83 +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 max-classes-per-file */ - -import { Registry } from './registry'; - -export interface ExpressionRenderDefinition { - name: string; - displayName: string; - help?: string; - validate?: () => void | Error; - reuseDomNode: boolean; - render: (domNode: HTMLElement, config: Config, handlers: any) => Promise; -} - -class ExpressionRenderFunction { - /** - * This must match the name of the function that is used to create the `type: render` object. - */ - name: string; - - /** - * Use this to set a more friendly name. - */ - displayName: string; - - /** - * A sentence or few about what this element does. - */ - help: string; - - /** - * Used to validate the data before calling the render function. - */ - validate: () => void | Error; - - /** - * Tell the renderer if the dom node should be reused, it's recreated each time by default. - */ - reuseDomNode: boolean; - - /** - * The function called to render the data. - */ - render: (domNode: HTMLElement, config: any, handlers: any) => Promise; - - constructor(config: ExpressionRenderDefinition) { - const { name, displayName, help, validate, reuseDomNode, render } = config; - - this.name = name; - this.displayName = displayName || name; - this.help = help || ''; - this.validate = validate || (() => {}); - this.reuseDomNode = Boolean(reuseDomNode); - this.render = render; - } -} - -export class RenderFunctionsRegistry extends Registry { - register(definition: ExpressionRenderDefinition | (() => ExpressionRenderDefinition)) { - const renderFunction = new ExpressionRenderFunction( - typeof definition === 'object' ? definition : definition() - ); - this.set(renderFunction.name, renderFunction); - } -} diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts index 56eb43a9bd133..b9601f6d1e920 100644 --- a/src/plugins/expressions/public/render.test.ts +++ b/src/plugins/expressions/public/render.test.ts @@ -19,9 +19,10 @@ import { ExpressionRenderHandler, render } from './render'; import { Observable } from 'rxjs'; -import { IInterpreterRenderHandlers, RenderError } from './types'; +import { RenderError } from './types'; import { getRenderersRegistry } from './services'; import { first, take, toArray } from 'rxjs/operators'; +import { IInterpreterRenderHandlers } from '../common'; const element: HTMLElement = {} as HTMLElement; const mockNotificationService = { diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 62bde12490fbe..86e360f8135e7 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -20,16 +20,10 @@ import * as Rx from 'rxjs'; import { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { - Data, - event, - IInterpreterRenderHandlers, - RenderError, - RenderErrorHandlerFnType, - RenderId, -} from './types'; +import { RenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types'; import { getRenderersRegistry } from './services'; import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler'; +import { IInterpreterRenderHandlers, ExpressionAstExpression } from '../common'; export type IExpressionRendererExtraHandlers = Record; @@ -37,17 +31,27 @@ export interface ExpressionRenderHandlerParams { onRenderError: RenderErrorHandlerFnType; } +interface Event { + name: string; + data: any; +} + +interface UpdateValue { + newExpression?: string | ExpressionAstExpression; + newParams: IExpressionLoaderParams; +} + export class ExpressionRenderHandler { - render$: Observable; - update$: Observable; - events$: Observable; + render$: Observable; + update$: Observable; + events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.BehaviorSubject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; - private updateSubject: Rx.Subject; + private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; private onRenderError: RenderErrorHandlerFnType; @@ -58,13 +62,13 @@ export class ExpressionRenderHandler { this.element = element; this.eventsSubject = new Rx.Subject(); - this.events$ = this.eventsSubject.asObservable(); + this.events$ = this.eventsSubject.asObservable() as Observable; this.onRenderError = onRenderError || defaultRenderErrorHandler; - this.renderSubject = new Rx.BehaviorSubject(null as RenderId | null); + this.renderSubject = new Rx.BehaviorSubject(null as any | null); this.render$ = this.renderSubject.asObservable().pipe(filter(_ => _ !== null)) as Observable< - RenderId + any >; this.updateSubject = new Rx.Subject(); @@ -90,7 +94,7 @@ export class ExpressionRenderHandler { }; } - render = async (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { + render = async (data: any, extraHandlers: IExpressionRendererExtraHandlers = {}) => { if (!data || typeof data !== 'object') { return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } @@ -113,7 +117,10 @@ export class ExpressionRenderHandler { // Rendering is asynchronous, completed by handlers.done() await getRenderersRegistry() .get(data.as)! - .render(this.element, data.value, { ...this.handlers, ...extraHandlers }); + .render(this.element, data.value, { + ...this.handlers, + ...extraHandlers, + } as any); } catch (e) { return this.handleRenderError(e); } @@ -139,7 +146,7 @@ export class ExpressionRenderHandler { export function render( element: HTMLElement, - data: Data, + data: any, options?: Partial ): ExpressionRenderHandler { const handler = new ExpressionRenderHandler(element, options); diff --git a/src/plugins/expressions/public/render_error_handler.ts b/src/plugins/expressions/public/render_error_handler.ts index 4d6bee1e375e0..432ef3ed96536 100644 --- a/src/plugins/expressions/public/render_error_handler.ts +++ b/src/plugins/expressions/public/render_error_handler.ts @@ -18,8 +18,9 @@ */ import { i18n } from '@kbn/i18n'; -import { RenderErrorHandlerFnType, IInterpreterRenderHandlers, RenderError } from './types'; +import { RenderErrorHandlerFnType, RenderError } from './types'; import { getNotifications } from './services'; +import { IInterpreterRenderHandlers } from '../common'; export const renderErrorHandler: RenderErrorHandlerFnType = ( element: HTMLElement, diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index e094e5e91d006..c77698d3661c2 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -17,42 +17,33 @@ * under the License. */ -import { ExpressionInterpret } from '../interpreter_provider'; -import { TimeRange, Query, esFilters } from '../../../data/public'; import { Adapters } from '../../../inspector/public'; -import { ExpressionRenderDefinition } from '../registries'; - -export type ExpressionInterpretWithHandlers = ( - ast: Parameters[0], - context: Parameters[1], - handlers: IInterpreterHandlers -) => ReturnType; - -export interface ExpressionInterpreter { - interpretAst: ExpressionInterpretWithHandlers; -} - +import { + IInterpreterRenderHandlers, + ExpressionValue, + ExecutionContextSearch, + ExpressionsService, +} from '../../common'; + +/** + * @deprecated + * + * This type if remainder from legacy platform, will be deleted going further. + */ export interface ExpressionExecutor { interpreter: ExpressionInterpreter; } -export type RenderId = number; -export type Data = any; -export type event = any; -export type Context = object; - -export interface SearchContext { - type: 'kibana_context'; - filters?: esFilters.Filter[]; - query?: Query; - timeRange?: TimeRange; +/** + * @deprecated + */ +export interface ExpressionInterpreter { + interpretAst: ExpressionsService['run']; } -export type IGetInitialContext = () => SearchContext | Context; - export interface IExpressionLoaderParams { - searchContext?: SearchContext; - context?: Context; + searchContext?: ExecutionContextSearch; + context?: ExpressionValue; variables?: Record; disableCaching?: boolean; customFunctions?: []; @@ -62,49 +53,6 @@ export interface IExpressionLoaderParams { onRenderError?: RenderErrorHandlerFnType; } -export interface IInterpreterHandlers { - getInitialContext: IGetInitialContext; - inspectorAdapters?: Adapters; - variables?: Record; - abortSignal?: AbortSignal; -} - -export interface IInterpreterRenderHandlers { - /** - * Done increments the number of rendering successes - */ - done: () => void; - onDestroy: (fn: () => void) => void; - reload: () => void; - update: (params: any) => void; - event: (event: event) => void; -} - -export interface IInterpreterRenderFunction { - name: string; - displayName: string; - help: string; - validate: () => void; - reuseDomNode: boolean; - render: (domNode: Element, data: T, handlers: IInterpreterRenderHandlers) => void | Promise; -} - -export interface IInterpreterErrorResult { - type: 'error'; - error: { message: string; name: string; stack: string }; -} - -export interface IInterpreterSuccessResult { - type: string; - as?: string; - value?: unknown; - error?: unknown; -} - -export type IInterpreterResult = IInterpreterSuccessResult & IInterpreterErrorResult; - -export { ExpressionRenderDefinition }; - export interface RenderError extends Error { type?: string; } diff --git a/src/plugins/expressions/server/legacy.ts b/src/plugins/expressions/server/legacy.ts index 54e2a5a387342..17aa1c66a6835 100644 --- a/src/plugins/expressions/server/legacy.ts +++ b/src/plugins/expressions/server/legacy.ts @@ -28,12 +28,12 @@ import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { CoreSetup, Logger } from 'src/core/server'; import { ExpressionsServerSetupDependencies } from './plugin'; -import { typeSpecs as types, Type } from '../common'; +import { typeSpecs, ExpressionType } from '../common'; import { serializeProvider } from '../common'; export class TypesRegistry extends Registry { wrapper(obj: any) { - return new (Type as any)(obj); + return new (ExpressionType as any)(obj); } } @@ -57,7 +57,7 @@ export const createLegacyServerInterpreterApi = (): LegacyInterpreterServerApi = const api = registryFactory(registries); register(registries, { - types, + types: typeSpecs, }); return api; diff --git a/src/plugins/expressions/server/plugin.ts b/src/plugins/expressions/server/plugin.ts index 84c780b5ca226..49229b6868062 100644 --- a/src/plugins/expressions/server/plugin.ts +++ b/src/plugins/expressions/server/plugin.ts @@ -24,6 +24,7 @@ import { createLegacyServerInterpreterApi, createLegacyServerEndpoints, } from './legacy'; +import { ExpressionsService } from '../common'; // eslint-disable-next-line export interface ExpressionsServerSetupDependencies { @@ -50,6 +51,8 @@ export class ExpressionsServerPlugin ExpressionsServerSetupDependencies, ExpressionsServerStartDependencies > { + readonly expressions: ExpressionsService = new ExpressionsService(); + constructor(private readonly initializerContext: PluginInitializerContext) {} public setup( @@ -57,6 +60,12 @@ export class ExpressionsServerPlugin plugins: ExpressionsServerSetupDependencies ): ExpressionsServerSetup { const logger = this.initializerContext.logger.get(); + const { expressions } = this; + const { executor } = expressions; + + executor.extendContext({ + environment: 'server', + }); const legacyApi = createLegacyServerInterpreterApi(); createLegacyServerEndpoints(legacyApi, logger, core, plugins); diff --git a/src/plugins/inspector/public/adapters/data/data_adapter.ts b/src/plugins/inspector/common/adapters/data/data_adapter.ts similarity index 100% rename from src/plugins/inspector/public/adapters/data/data_adapter.ts rename to src/plugins/inspector/common/adapters/data/data_adapter.ts diff --git a/src/plugins/inspector/public/adapters/data/data_adapters.test.ts b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts similarity index 100% rename from src/plugins/inspector/public/adapters/data/data_adapters.test.ts rename to src/plugins/inspector/common/adapters/data/data_adapters.test.ts diff --git a/src/plugins/inspector/public/adapters/data/formatted_data.ts b/src/plugins/inspector/common/adapters/data/formatted_data.ts similarity index 100% rename from src/plugins/inspector/public/adapters/data/formatted_data.ts rename to src/plugins/inspector/common/adapters/data/formatted_data.ts diff --git a/src/plugins/inspector/public/adapters/data/index.ts b/src/plugins/inspector/common/adapters/data/index.ts similarity index 100% rename from src/plugins/inspector/public/adapters/data/index.ts rename to src/plugins/inspector/common/adapters/data/index.ts diff --git a/src/plugins/inspector/public/adapters/data/types.ts b/src/plugins/inspector/common/adapters/data/types.ts similarity index 100% rename from src/plugins/inspector/public/adapters/data/types.ts rename to src/plugins/inspector/common/adapters/data/types.ts diff --git a/src/plugins/inspector/public/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts similarity index 100% rename from src/plugins/inspector/public/adapters/index.ts rename to src/plugins/inspector/common/adapters/index.ts diff --git a/src/plugins/inspector/public/adapters/request/index.ts b/src/plugins/inspector/common/adapters/request/index.ts similarity index 100% rename from src/plugins/inspector/public/adapters/request/index.ts rename to src/plugins/inspector/common/adapters/request/index.ts diff --git a/src/plugins/inspector/public/adapters/request/request_adapter.test.ts b/src/plugins/inspector/common/adapters/request/request_adapter.test.ts similarity index 100% rename from src/plugins/inspector/public/adapters/request/request_adapter.test.ts rename to src/plugins/inspector/common/adapters/request/request_adapter.test.ts diff --git a/src/plugins/inspector/public/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts similarity index 100% rename from src/plugins/inspector/public/adapters/request/request_adapter.ts rename to src/plugins/inspector/common/adapters/request/request_adapter.ts diff --git a/src/plugins/inspector/public/adapters/request/request_responder.ts b/src/plugins/inspector/common/adapters/request/request_responder.ts similarity index 100% rename from src/plugins/inspector/public/adapters/request/request_responder.ts rename to src/plugins/inspector/common/adapters/request/request_responder.ts diff --git a/src/plugins/inspector/public/adapters/request/types.ts b/src/plugins/inspector/common/adapters/request/types.ts similarity index 100% rename from src/plugins/inspector/public/adapters/request/types.ts rename to src/plugins/inspector/common/adapters/request/types.ts diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts new file mode 100644 index 0000000000000..06ab36a577d98 --- /dev/null +++ b/src/plugins/inspector/common/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 './adapters'; diff --git a/src/plugins/inspector/index.ts b/src/plugins/inspector/index.ts new file mode 100644 index 0000000000000..a9794d9e4647a --- /dev/null +++ b/src/plugins/inspector/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 './common'; diff --git a/src/plugins/inspector/public/index.ts b/src/plugins/inspector/public/index.ts index ea3985563118b..e90e05aa2830a 100644 --- a/src/plugins/inspector/public/index.ts +++ b/src/plugins/inspector/public/index.ts @@ -26,4 +26,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { InspectorPublicPlugin as Plugin, Setup, Start } from './plugin'; export * from './types'; -export * from './adapters'; +export * from '../common/adapters'; diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts index 1aeffd68a9f3d..0604129a0734a 100644 --- a/src/plugins/inspector/public/test/is_available.test.ts +++ b/src/plugins/inspector/public/test/is_available.test.ts @@ -18,8 +18,8 @@ */ import { inspectorPluginMock } from '../mocks'; -import { DataAdapter } from '../adapters/data/data_adapter'; -import { RequestAdapter } from '../adapters/request/request_adapter'; +import { DataAdapter } from '../../common/adapters/data/data_adapter'; +import { RequestAdapter } from '../../common/adapters/request/request_adapter'; const adapter1 = new DataAdapter(); const adapter2 = new RequestAdapter(); diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/inspector/public/views/data/components/data_table.tsx index b78a3920804d2..69be069272f79 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/inspector/public/views/data/components/data_table.tsx @@ -35,7 +35,7 @@ import { i18n } from '@kbn/i18n'; import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; -import { TabularData } from '../../../adapters/data/types'; +import { TabularData } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; interface DataTableFormatState { diff --git a/src/plugins/inspector/public/views/data/components/data_view.test.tsx b/src/plugins/inspector/public/views/data/components/data_view.test.tsx index 55322bf5ec91a..2772069d36877 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.test.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { getDataViewDescription } from '../index'; -import { DataAdapter } from '../../../adapters/data'; +import { DataAdapter } from '../../../../common/adapters/data'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { IUiSettingsClient } from '../../../../../../core/public'; diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index 91f42a54f64d0..e03c165d96a27 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -31,7 +31,11 @@ import { import { DataTableFormat } from './data_table'; import { InspectorViewProps, Adapters } from '../../../types'; -import { TabularLoaderOptions, TabularData, TabularCallback } from '../../../adapters/data/types'; +import { + TabularLoaderOptions, + TabularData, + TabularCallback, +} from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; interface DataViewComponentState { diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx index 24412e860f73c..d7cb8f5745613 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx @@ -20,7 +20,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiCodeBlock } from '@elastic/eui'; -import { Request } from '../../../../adapters/request/types'; +import { Request } from '../../../../../common/adapters/request/types'; import { RequestDetailsProps } from '../types'; export class RequestDetailsRequest extends Component { diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx index f72cde24854a2..933495ff47396 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx @@ -20,7 +20,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiCodeBlock } from '@elastic/eui'; -import { Request } from '../../../../adapters/request/types'; +import { Request } from '../../../../../common/adapters/request/types'; import { RequestDetailsProps } from '../types'; export class RequestDetailsResponse extends Component { diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx index c58795d09946c..767f1c2c5ebcf 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx @@ -28,7 +28,7 @@ import { EuiTableRowCell, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Request, RequestStatistic } from '../../../../adapters/request/types'; +import { Request, RequestStatistic } from '../../../../../common/adapters/request/types'; import { RequestDetailsProps } from '../types'; // TODO: Replace by property once available diff --git a/src/plugins/inspector/public/views/requests/components/request_selector.tsx b/src/plugins/inspector/public/views/requests/components/request_selector.tsx index 535ce8ef4b7fc..7971f44be6ebd 100644 --- a/src/plugins/inspector/public/views/requests/components/request_selector.tsx +++ b/src/plugins/inspector/public/views/requests/components/request_selector.tsx @@ -35,8 +35,8 @@ import { EuiToolTip, } from '@elastic/eui'; -import { RequestStatus } from '../../../adapters'; -import { Request } from '../../../adapters/request/types'; +import { RequestStatus } from '../../../../common/adapters'; +import { Request } from '../../../../common/adapters/request/types'; interface RequestSelectorState { isPopoverOpen: boolean; diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index 01ae5e739c93b..a433ea70dc35c 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -22,8 +22,8 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; -import { RequestStatus } from '../../../adapters'; -import { Request } from '../../../adapters/request/types'; +import { RequestStatus } from '../../../../common/adapters'; +import { Request } from '../../../../common/adapters/request/types'; import { InspectorViewProps } from '../../../types'; import { RequestSelector } from './request_selector'; diff --git a/src/plugins/inspector/public/views/requests/components/types.ts b/src/plugins/inspector/public/views/requests/components/types.ts index ebc3b41e41019..54ba8f0636c1e 100644 --- a/src/plugins/inspector/public/views/requests/components/types.ts +++ b/src/plugins/inspector/public/views/requests/components/types.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from '../../../adapters/request/types'; +import { Request } from '../../../../common/adapters/request/types'; export interface RequestDetailsProps { request: Request; diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts index 78bfc0c3e9090..c6e1f53145312 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -36,7 +36,7 @@ const isProduction = ? process.env.NODE_ENV === 'production' : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; -const freeze: (value: T) => T = isProduction +const defaultFreeze: (value: T) => T = isProduction ? (value: T) => value as T : (value: T): T => { const isFreezable = value !== null && typeof value === 'object'; @@ -44,6 +44,22 @@ const freeze: (value: T) => T = isProduction return value as T; }; +export interface CreateStateContainerOptions { + /** + * Function to use when freezing state. Supply identity function + * + * ```ts + * { + * freeze: state => state, + * } + * ``` + * + * if you expect that your state will be mutated externally an you cannot + * prevent that. + */ + freeze?: (state: T) => T; +} + export function createStateContainer( defaultState: State ): ReduxLikeStateContainer; @@ -58,7 +74,8 @@ export function createStateContainer< >( defaultState: State, pureTransitions: PureTransitions, - pureSelectors: PureSelectors + pureSelectors: PureSelectors, + options?: CreateStateContainerOptions ): ReduxLikeStateContainer; export function createStateContainer< State extends BaseState, @@ -67,8 +84,10 @@ export function createStateContainer< >( defaultState: State, pureTransitions: PureTransitions = {} as PureTransitions, - pureSelectors: PureSelectors = {} as PureSelectors + pureSelectors: PureSelectors = {} as PureSelectors, + options: CreateStateContainerOptions = {} ): ReduxLikeStateContainer { + const { freeze = defaultFreeze } = options; const data$ = new BehaviorSubject(freeze(defaultState)); const state$ = data$.pipe(skip(1)); const get = () => data$.getValue(); diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx index 97ac25dca8cf1..90212fbe83c10 100644 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx +++ b/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx @@ -695,4 +695,34 @@ describe('SavedObjectsFinder', () => { expect(wrapper.containsMatchingElement()).toBe(false); }); }); + + it('should render with children', async () => { + const core = coreMock.createStart(); + ((core.savedObjects.client.find as any) as jest.SpyInstance).mockImplementation(() => + Promise.resolve({ savedObjects: [doc, doc2] }) + ); + core.uiSettings.get.mockImplementation(() => 10); + + const wrapper = shallow( + 'search', + }, + { + type: 'vis', + name: 'Vis', + getIconForSavedObject: () => 'visLine', + }, + ]} + > + + + ); + expect(wrapper.exists('#testChildButton')).toBe(true); + }); }); diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx index 0658ed64df84c..b503392c9827f 100644 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx @@ -438,6 +438,7 @@ class SavedObjectFinderUi extends React.Component< )} + {this.props.children ? {this.props.children} : null} ); } diff --git a/src/plugins/visualizations/public/expression_functions/range.ts b/src/plugins/visualizations/public/expression_functions/range.ts index 27c3654e2182a..42eb6aa781970 100644 --- a/src/plugins/visualizations/public/expression_functions/range.ts +++ b/src/plugins/visualizations/public/expression_functions/range.ts @@ -18,19 +18,20 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaDatatable, Range } from '../../../expressions/public'; - -const name = 'range'; - -type Context = KibanaDatatable | null; +import { ExpressionFunctionDefinition, KibanaDatatable, Range } from '../../../expressions/public'; interface Arguments { from: number; to: number; } -export const range = (): ExpressionFunction => ({ - name, +export const range = (): ExpressionFunctionDefinition< + 'range', + KibanaDatatable | null, + Arguments, + Range +> => ({ + name: 'range', help: i18n.translate('visualizations.function.range.help', { defaultMessage: 'Generates range object', }), diff --git a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts index 4ad73ef504874..b9d1a23b1c503 100644 --- a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts +++ b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts @@ -18,11 +18,12 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaDatatable } from '../../../expressions/public'; - -const name = 'visdimension'; - -type Context = KibanaDatatable | null; +import { + ExpressionFunctionDefinition, + ExpressionValueBoxed, + KibanaDatatable, + KibanaDatatableColumn, +} from '../../../expressions/public'; interface Arguments { accessor: string | number; @@ -30,17 +31,29 @@ interface Arguments { formatParams?: string; } -type Return = any; +type ExpressionValueVisDimension = ExpressionValueBoxed< + 'vis_dimension', + { + accessor: number | KibanaDatatableColumn; + format: { + id?: string; + params: unknown; + }; + } +>; -export const visDimension = (): ExpressionFunction => ({ +export const visDimension = (): ExpressionFunctionDefinition< + 'visdimension', + KibanaDatatable, + Arguments, + ExpressionValueVisDimension +> => ({ name: 'visdimension', help: i18n.translate('visualizations.function.visDimension.help', { defaultMessage: 'Generates visConfig dimension object', }), type: 'vis_dimension', - context: { - types: ['kibana_datatable'], - }, + inputTypes: ['kibana_datatable'], args: { accessor: { types: ['string', 'number'], @@ -64,11 +77,12 @@ export const visDimension = (): ExpressionFunction { + fn: (input, args) => { const accessor = typeof args.accessor === 'number' ? args.accessor - : context!.columns.find(c => c.id === args.accessor); + : input.columns.find(c => c.id === args.accessor); + if (accessor === undefined) { throw new Error( i18n.translate('visualizations.function.visDimension.error.accessor', { diff --git a/test/interpreter_functional/README.md b/test/interpreter_functional/README.md index 73df0ce4c9f04..928792ff8d484 100644 --- a/test/interpreter_functional/README.md +++ b/test/interpreter_functional/README.md @@ -22,6 +22,7 @@ node scripts/functional_test_runner.js --config test/interpreter_functional/conf 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.ts --updateBaselines -``` \ No newline at end of file +``` 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 index daa19f22a7023..41e466fddd11e 100644 --- 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 @@ -20,27 +20,19 @@ 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, - RenderId, -} from '../../types'; +import { IInterpreterRenderHandlers, ExpressionValue } from 'src/plugins/expressions'; +import { RequestAdapter, DataAdapter } from '../../../../../../../../src/plugins/inspector'; +import { Adapters, ExpressionRenderHandler, ExpressionDataHandler } from '../../types'; import { getExpressions } from '../../services'; declare global { interface Window { runPipeline: ( expressions: string, - context?: Context, - initialContext?: Context + context?: ExpressionValue, + initialContext?: ExpressionValue ) => ReturnType; - renderPipelineResponse: (context?: Context) => Promise; + renderPipelineResponse: (context?: ExpressionValue) => Promise; } } @@ -60,8 +52,8 @@ class Main extends React.Component<{}, State> { window.runPipeline = async ( expression: string, - context: Context = {}, - initialContext: Context = {} + context: ExpressionValue = {}, + initialContext: ExpressionValue = {} ) => { this.setState({ expression }); const adapters: Adapters = { @@ -86,7 +78,7 @@ class Main extends React.Component<{}, State> { } lastRenderHandler = getExpressions().render(this.chartRef.current!, context, { - onRenderError: (el, error, handler) => { + onRenderError: (el: HTMLElement, error: unknown, handler: IInterpreterRenderHandlers) => { this.setState({ expression: 'Render error!\n\n' + JSON.stringify(error), }); 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 index cc4190bd099fa..6e0a93e4a3cb1 100644 --- 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 @@ -19,19 +19,10 @@ import { ExpressionsStart, - Context, ExpressionRenderHandler, ExpressionDataHandler, - RenderId, } from 'src/plugins/expressions/public'; import { Adapters } from 'src/plugins/inspector/public'; -export { - ExpressionsStart, - Context, - ExpressionRenderHandler, - ExpressionDataHandler, - RenderId, - Adapters, -}; +export { ExpressionsStart, ExpressionRenderHandler, ExpressionDataHandler, Adapters }; diff --git a/test/interpreter_functional/snapshots/baseline/combined_test0.json b/test/interpreter_functional/snapshots/baseline/combined_test0.json index 2af0407f0d521..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test0.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test0.json @@ -1 +1 @@ -{"filters":null,"query":null,"timeRange":null,"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_test0.json b/test/interpreter_functional/snapshots/baseline/step_output_test0.json index 2af0407f0d521..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test0.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test0.json @@ -1 +1 @@ -{"filters":null,"query":null,"timeRange":null,"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/session/combined_test0.json b/test/interpreter_functional/snapshots/session/combined_test0.json index 2af0407f0d521..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/session/combined_test0.json +++ b/test/interpreter_functional/snapshots/session/combined_test0.json @@ -1 +1 @@ -{"filters":null,"query":null,"timeRange":null,"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/session/step_output_test0.json b/test/interpreter_functional/snapshots/session/step_output_test0.json index 2af0407f0d521..8f00d72df8ab3 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test0.json +++ b/test/interpreter_functional/snapshots/session/step_output_test0.json @@ -1 +1 @@ -{"filters":null,"query":null,"timeRange":null,"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/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 7fedf1723908a..015c311c30aef 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -18,12 +18,9 @@ */ import expect from '@kbn/expect'; +import { ExpressionValue } from 'src/plugins/expressions'; import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -import { - ExpressionDataHandler, - Context, - RenderId, -} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; +import { ExpressionDataHandler } from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; type UnWrapPromise = T extends Promise ? U : T; export type ExpressionResult = UnWrapPromise>; @@ -31,14 +28,14 @@ export type ExpressionResult = UnWrapPromise ExpectExpressionHandler; export interface ExpectExpressionHandler { toReturn: (expectedResult: ExpressionResult) => Promise; getResponse: () => Promise; - runExpression: (step?: string, stepContext?: Context) => Promise; + runExpression: (step?: string, stepContext?: ExpressionValue) => Promise; steps: { toMatchSnapshot: () => Promise; }; @@ -68,8 +65,8 @@ export function expectExpressionProvider({ return ( name: string, expression: string, - context: Context = {}, - initialContext: Context = {} + context: ExpressionValue = {}, + initialContext: ExpressionValue = {} ): ExpectExpressionHandler => { log.debug(`executing expression ${expression}`); const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast @@ -101,14 +98,14 @@ export function expectExpressionProvider({ */ runExpression: async ( step: string = expression, - stepContext: Context = context + stepContext: ExpressionValue = context ): Promise => { log.debug(`running expression ${step || expression}`); return browser.executeAsync( ( _expression: string, - _currentContext: Context & { type: string }, - _initialContext: Context, + _currentContext: ExpressionValue & { type: string }, + _initialContext: ExpressionValue, done: (expressionResult: ExpressionResult) => void ) => { if (!_currentContext) _currentContext = { type: 'null' }; @@ -168,8 +165,8 @@ export function expectExpressionProvider({ toMatchScreenshot: async () => { const pipelineResponse = await handler.getResponse(); log.debug('starting to render'); - const result = await browser.executeAsync( - (_context: ExpressionResult, done: (renderResult: RenderId) => void) => + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: any) => void) => window.renderPipelineResponse(_context).then(renderResult => { done(renderResult); return renderResult; diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index 019194716b230..e566952eea86b 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -173,6 +173,7 @@ module.exports = async ({ config }) => { '../tasks/mocks/uiNotifyFormatMsg' ); config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); + config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl'); config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts index 8cb0b26565ef3..3ed08268222d0 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/function_specs.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore Untyped Library -import { Fn } from '@kbn/interpreter/common'; -import { uniq } from 'lodash'; import { functions as browserFns } from '../../canvas_plugin_src/functions/browser'; -import { functions as commonFns } from '../../canvas_plugin_src/functions/common'; -import { functions as serverFns } from '../../canvas_plugin_src/functions/server'; +import { ExpressionFunction } from '../../../../../../src/plugins/expressions'; -export const functionSpecs = uniq([...browserFns, ...commonFns, ...serverFns], 'name').map(fn => new Fn(fn())); +export const functionSpecs = browserFns.map(fn => new ExpressionFunction(fn())); diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js index 141beb3d34d78..8caab5c41563c 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js @@ -23,7 +23,6 @@ export class Plugin { [this.props.name]: {}, elasticsearch: mockElasticsearch, }, - injectUiAppVars: noop, config: () => ({ get: key => get(config, key), has: key => has(config, key), diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index e728ea25f5504..fbe7825c3b2c8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionType } from 'src/plugins/expressions/public'; +import { ExpressionTypeDefinition } from '../../../../../../src/plugins/expressions'; import { EmbeddableInput } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableTypes } from './embeddable_types'; @@ -17,7 +17,7 @@ export interface EmbeddableExpression { embeddableType: string; } -export const embeddableType = (): ExpressionType< +export const embeddableType = (): ExpressionTypeDefinition< typeof EmbeddableExpressionType, EmbeddableExpression > => ({ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts index af4d0a4ffda92..1e13ebdee3e4b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, ExpressionFunction } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; const noop = () => {}; @@ -14,15 +14,13 @@ interface Return extends Datatable { rows: [{ latitude: number; longitude: number }]; } -export function location(): ExpressionFunction<'location', null, {}, Promise> { +export function location(): ExpressionFunctionDefinition<'location', null, {}, Promise> { const { help } = getFunctionHelp().location; return { name: 'location', type: 'datatable', - context: { - types: ['null'], - }, + inputTypes: ['null'], args: {}, help, fn: () => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts index 364dd2eb426fa..95859feeed5f3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, Render, Style, ExpressionFunction } from 'src/plugins/expressions/common'; +import { + Datatable, + Render, + Style, + ExpressionFunctionDefinition, +} from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { Handlebars } from '../../../common/lib/handlebars'; import { getFunctionHelp } from '../../../i18n'; @@ -21,7 +26,12 @@ interface Return { font: Style; } -export function markdown(): ExpressionFunction<'markdown', Context, Arguments, Render> { +export function markdown(): ExpressionFunctionDefinition< + 'markdown', + Context, + Arguments, + Render +> { const { help, args: argHelp } = getFunctionHelp().markdown; return { @@ -29,9 +39,7 @@ export function markdown(): ExpressionFunction<'markdown', Context, Arguments, R aliases: [], type: 'render', help, - context: { - types: ['datatable', 'null'], - }, + inputTypes: ['datatable', 'null'], args: { content: { aliases: ['_', 'expression'], @@ -46,7 +54,7 @@ export function markdown(): ExpressionFunction<'markdown', Context, Arguments, R default: '{font}', }, }, - fn: (context, args) => { + fn: (input, args) => { const compileFunctions = args.content.map(str => Handlebars.compile(String(str), { knownHelpersOnly: true }) ); @@ -54,7 +62,7 @@ export function markdown(): ExpressionFunction<'markdown', Context, Arguments, R columns: [], rows: [], type: null, - ...context, + ...input, }; return { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts index c7109adffd481..0fcde6cbcf309 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts @@ -5,7 +5,7 @@ */ import { parse } from 'url'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,7 +13,12 @@ interface Arguments { default: string; } -export function urlparam(): ExpressionFunction<'urlparam', null, Arguments, string | string[]> { +export function urlparam(): ExpressionFunctionDefinition< + 'urlparam', + null, + Arguments, + string | string[] +> { const { help, args: argHelp } = getFunctionHelp().urlparam; return { @@ -21,9 +26,7 @@ export function urlparam(): ExpressionFunction<'urlparam', null, Arguments, stri aliases: [], type: 'string', help, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { param: { types: ['string'], @@ -38,7 +41,7 @@ export function urlparam(): ExpressionFunction<'urlparam', null, Arguments, stri help: argHelp.default, }, }, - fn: (_context, args) => { + fn: (input, args) => { const query = parse(window.location.href, true).query; return query[args.param] || args.default; }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/all.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/all.ts index 821ab520d8897..812341db0198f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/all.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/all.ts @@ -4,23 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { condition: boolean[]; } -export function all(): ExpressionFunction<'all', null, Arguments, boolean> { +export function all(): ExpressionFunctionDefinition<'all', null, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().all; return { name: 'all', type: 'boolean', help, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { condition: { aliases: ['_'], @@ -30,7 +28,7 @@ export function all(): ExpressionFunction<'all', null, Arguments, boolean> { multi: true, }, }, - fn: (_context, args) => { + fn: (input, args) => { const conditions = args.condition || []; return conditions.every(Boolean); }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts index c87d136007b9b..e6739a71b1608 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts @@ -6,7 +6,7 @@ import { omit } from 'lodash'; import { Datatable } from 'src/plugins/expressions/common'; -import { DatatableColumn, DatatableColumnType, ExpressionFunction } from '../../../types'; +import { DatatableColumn, DatatableColumnType, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -15,17 +15,20 @@ interface Arguments { name: string; } -export function alterColumn(): ExpressionFunction<'alterColumn', Datatable, Arguments, Datatable> { +export function alterColumn(): ExpressionFunctionDefinition< + 'alterColumn', + Datatable, + Arguments, + Datatable +> { const { help, args: argHelp } = getFunctionHelp().alterColumn; const errors = getFunctionErrors().alterColumn; return { name: 'alterColumn', type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { column: { aliases: ['_'], @@ -43,12 +46,12 @@ export function alterColumn(): ExpressionFunction<'alterColumn', Datatable, Argu options: ['null', 'boolean', 'number', 'string', 'date'], }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.column || (!args.type && !args.name)) { - return context; + return input; } - const column = context.columns.find(col => col.name === args.column); + const column = input.columns.find(col => col.name === args.column); if (!column) { throw errors.columnNotFound(args.column); } @@ -56,7 +59,7 @@ export function alterColumn(): ExpressionFunction<'alterColumn', Datatable, Argu const name = args.name || column.name; const type = args.type || column.type; - const columns = context.columns.reduce((all: DatatableColumn[], col) => { + const columns = input.columns.reduce((all: DatatableColumn[], col) => { if (col.name !== args.name) { if (col.name !== column.name) { all.push(col); @@ -91,7 +94,7 @@ export function alterColumn(): ExpressionFunction<'alterColumn', Datatable, Argu })(); } - const rows = context.rows.map(row => ({ + const rows = input.rows.map(row => ({ ...omit(row, column.name), [name]: handler(row[column.name]), })); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/any.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/any.ts index 8f86351dcad82..4b8097d36cf5d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/any.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/any.ts @@ -4,22 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { condition: boolean[]; } -export function any(): ExpressionFunction<'any', null, Arguments, boolean> { +export function any(): ExpressionFunctionDefinition<'any', null, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().any; return { name: 'any', type: 'boolean', - context: { - types: ['null'], - }, + inputTypes: ['null'], help, args: { condition: { @@ -30,7 +28,7 @@ export function any(): ExpressionFunction<'any', null, Arguments, boolean> { help: argHelp.condition, }, }, - fn: (_context, args) => { + fn: (input, args) => { const conditions = args.condition || []; return conditions.some(Boolean); }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/as.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/as.ts index ffb493f76e739..9c10e85227398 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/as.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/as.ts @@ -4,24 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, ExpressionFunction, getType } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { name: string; } -type Context = string | boolean | number | null; +type Input = string | boolean | number | null; -export function asFn(): ExpressionFunction<'as', Context, Arguments, Datatable> { +export function asFn(): ExpressionFunctionDefinition<'as', Input, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().as; return { name: 'as', type: 'datatable', - context: { - types: ['string', 'boolean', 'number', 'null'], - }, + inputTypes: ['string', 'boolean', 'number', 'null'], help, args: { name: { @@ -31,18 +29,18 @@ export function asFn(): ExpressionFunction<'as', Context, Arguments, Datatable> default: 'value', }, }, - fn: (context, args) => { + fn: (input, args) => { return { type: 'datatable', columns: [ { name: args.name, - type: getType(context), + type: getType(input), }, ], rows: [ { - [args.name]: context, + [args.name]: input, }, ], }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/axisConfig.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/axisConfig.ts index 76e69eb7caf72..47da6f0560302 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/axisConfig.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/axisConfig.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Position } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; @@ -21,7 +21,12 @@ interface AxisConfig extends Arguments { type: 'axisConfig'; } -export function axisConfig(): ExpressionFunction<'axisConfig', null, Arguments, AxisConfig> { +export function axisConfig(): ExpressionFunctionDefinition< + 'axisConfig', + null, + Arguments, + AxisConfig +> { const { help, args: argHelp } = getFunctionHelp().axisConfig; const errors = getFunctionErrors().axisConfig; @@ -29,10 +34,8 @@ export function axisConfig(): ExpressionFunction<'axisConfig', null, Arguments, name: 'axisConfig', aliases: [], type: 'axisConfig', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { max: { types: ['number', 'string', 'null'], @@ -58,7 +61,7 @@ export function axisConfig(): ExpressionFunction<'axisConfig', null, Arguments, help: argHelp.tickSize, }, }, - fn: (_context, args) => { + fn: (input, args) => { const { position, min, max, ...rest } = args; if (!Object.values(Position).includes(position)) { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/case.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/case.ts index e059910a948b8..dd573b1283915 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/case.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/case.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -18,7 +18,7 @@ interface Case { result: any; } -export function caseFn(): ExpressionFunction<'case', any, Arguments, Promise> { +export function caseFn(): ExpressionFunctionDefinition<'case', any, Arguments, Promise> { const { help, args: argHelp } = getFunctionHelp().case; return { @@ -41,9 +41,9 @@ export function caseFn(): ExpressionFunction<'case', any, Arguments, Promise { - const matches = await doesMatch(context, args); - const result = matches ? await getResult(context, args) : null; + fn: async (input, args) => { + const matches = await doesMatch(input, args); + const result = matches ? await getResult(input, args) : null; return { type: 'case', matches, result }; }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/clear.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/clear.ts index 51bcb9552e3dd..fe074190d2450 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/clear.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/clear.ts @@ -3,19 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; -export function clear(): ExpressionFunction<'clear', any, {}, null> { +export function clear(): ExpressionFunctionDefinition<'clear', any, {}, null> { const { help } = getFunctionHelp().clear; return { name: 'clear', type: 'null', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: {}, fn: () => null, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/columns.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/columns.ts index 8c1be7df1f208..71c5376428a79 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/columns.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/columns.ts @@ -5,7 +5,7 @@ */ import { omit, pick, find } from 'lodash'; -import { Datatable, DatatableColumn, ExpressionFunction } from '../../../types'; +import { Datatable, DatatableColumn, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,16 +13,19 @@ interface Arguments { exclude: string; } -export function columns(): ExpressionFunction<'columns', Datatable, Arguments, Datatable> { +export function columns(): ExpressionFunctionDefinition< + 'columns', + Datatable, + Arguments, + Datatable +> { const { help, args: argHelp } = getFunctionHelp().columns; return { name: 'columns', type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { include: { aliases: ['_'], @@ -34,10 +37,10 @@ export function columns(): ExpressionFunction<'columns', Datatable, Arguments, D help: argHelp.exclude, }, }, - fn: (context, args) => { + fn: (input, args) => { const { include, exclude } = args; - const { columns: contextColumns, rows: contextRows, ...rest } = context; - let result = { ...context }; + const { columns: contextColumns, rows: contextRows, ...rest } = input; + let result = { ...input }; if (exclude) { const fields = exclude.split(',').map(field => field.trim()); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/compare.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/compare.ts index 3e17fe9b89dab..e952faca1d5eb 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/compare.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/compare.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; export enum Operation { @@ -23,7 +23,7 @@ interface Arguments { type Context = boolean | number | string | null; -export function compare(): ExpressionFunction<'compare', Context, Arguments, boolean> { +export function compare(): ExpressionFunctionDefinition<'compare', Context, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().compare; const errors = getFunctionErrors().compare; @@ -32,9 +32,7 @@ export function compare(): ExpressionFunction<'compare', Context, Arguments, boo help, aliases: ['condition'], type: 'boolean', - context: { - types: ['string', 'number', 'boolean', 'null'], - }, + inputTypes: ['string', 'number', 'boolean', 'null'], args: { op: { aliases: ['_'], @@ -48,8 +46,8 @@ export function compare(): ExpressionFunction<'compare', Context, Arguments, boo help: argHelp.to, }, }, - fn: (context, args) => { - const a = context; + fn: (input, args) => { + const a = input; const { to: b, op } = args; const typesMatch = typeof a === typeof b; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts index fe399ce5970ed..b841fde284ab6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts @@ -3,21 +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 { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ContainerStyle, Overflow, BackgroundRepeat, BackgroundSize } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; // @ts-ignore untyped local import { isValidUrl } from '../../../common/lib/url'; -interface Return extends ContainerStyle { +interface Output extends ContainerStyle { type: 'containerStyle'; } -export function containerStyle(): ExpressionFunction< +export function containerStyle(): ExpressionFunctionDefinition< 'containerStyle', null, ContainerStyle, - Return + Output > { const { help, args: argHelp } = getFunctionHelp().containerStyle; const errors = getFunctionErrors().containerStyle; @@ -26,10 +26,8 @@ export function containerStyle(): ExpressionFunction< name: 'containerStyle', aliases: [], type: 'containerStyle', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { backgroundColor: { types: ['string'], @@ -74,12 +72,12 @@ export function containerStyle(): ExpressionFunction< help: argHelp.padding, }, }, - fn: (_context, args) => { + fn: (input, args) => { const { backgroundImage, backgroundSize, backgroundRepeat, ...remainingArgs } = args; const style = { type: 'containerStyle', ...remainingArgs, - } as Return; + } as Output; if (backgroundImage) { if (!isValidUrl(backgroundImage)) { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/context.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/context.ts index 021c6d529672c..d1302a1e579a1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/context.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/context.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; -export function context(): ExpressionFunction<'context', any, {}, any> { +export function context(): ExpressionFunctionDefinition<'context', unknown, {}, unknown> { const { help } = getFunctionHelp().context; return { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/csv.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/csv.ts index 753ab84f13207..705639baffc98 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/csv.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/csv.ts @@ -5,7 +5,7 @@ */ import Papa from 'papaparse'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Datatable } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; @@ -15,17 +15,15 @@ interface Arguments { newline: string; } -export function csv(): ExpressionFunction<'csv', null, Arguments, Datatable> { +export function csv(): ExpressionFunctionDefinition<'csv', null, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().csv; const errorMessages = getFunctionErrors().csv; return { name: 'csv', type: 'datatable', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { data: { aliases: ['_'], @@ -42,7 +40,7 @@ export function csv(): ExpressionFunction<'csv', null, Arguments, Datatable> { help: argHelp.newline, }, }, - fn(_context, args) { + fn(input, args) { const { data: csvString, delimiter, newline } = args; const config: Papa.ParseConfig = { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/date.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/date.ts index 67a557259709e..573ea8a855607 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/date.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/date.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -13,7 +13,7 @@ interface Arguments { format: string; } -export function date(): ExpressionFunction<'date', null, Arguments, number> { +export function date(): ExpressionFunctionDefinition<'date', null, Arguments, number> { const { help, args: argHelp } = getFunctionHelp().date; const errors = getFunctionErrors().date; @@ -21,9 +21,7 @@ export function date(): ExpressionFunction<'date', null, Arguments, number> { name: 'date', type: 'number', help, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { value: { aliases: ['_'], @@ -35,7 +33,7 @@ export function date(): ExpressionFunction<'date', null, Arguments, number> { help: argHelp.format, }, }, - fn: (_context, args) => { + fn: (input, args) => { const { value: argDate, format } = args; const outputDate = diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/do.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/do.ts index 5fafedaf58c80..5f0c848d76708 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/do.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/do.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { fn: any[]; } -export function doFn(): ExpressionFunction<'do', any, Arguments, any> { +export function doFn(): ExpressionFunctionDefinition<'do', unknown, Arguments, unknown> { const { help, args: argHelp } = getFunctionHelp().do; return { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts index a4bef4e5e40b2..29a277283494a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts @@ -5,7 +5,7 @@ */ import { uniq } from 'lodash'; -import { Datatable, Render, ExpressionFunction } from '../../../types'; +import { Datatable, Render, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -19,7 +19,7 @@ interface Return { choices: any; } -export function dropdownControl(): ExpressionFunction< +export function dropdownControl(): ExpressionFunctionDefinition< 'dropdownControl', Datatable, Arguments, @@ -31,9 +31,7 @@ export function dropdownControl(): ExpressionFunction< name: 'dropdownControl', aliases: [], type: 'render', - context: { - types: ['datatable'], - }, + inputTypes: ['datatable'], help, args: { filterColumn: { @@ -51,11 +49,11 @@ export function dropdownControl(): ExpressionFunction< help: argHelp.filterGroup, }, }, - fn: (context, { valueColumn, filterColumn, filterGroup }) => { + fn: (input, { valueColumn, filterColumn, filterGroup }) => { let choices = []; - if (context.rows[0][valueColumn]) { - choices = uniq(context.rows.map(row => row[valueColumn])).sort(); + if (input.rows[0][valueColumn]) { + choices = uniq(input.rows.map(row => row[valueColumn])).sort(); } const column = filterColumn || valueColumn; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/eq.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/eq.ts index 1df74c9d0b689..9cb28dea42607 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/eq.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/eq.ts @@ -3,25 +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 { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - value: Context; + value: Input; } -type Context = boolean | number | string | null; +type Input = boolean | number | string | null; -export function eq(): ExpressionFunction<'eq', Context, Arguments, boolean> { +export function eq(): ExpressionFunctionDefinition<'eq', Input, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().eq; return { name: 'eq', type: 'boolean', + inputTypes: ['boolean', 'number', 'string', 'null'], help, - context: { - types: ['boolean', 'number', 'string', 'null'], - }, args: { value: { aliases: ['_'], @@ -30,8 +28,8 @@ export function eq(): ExpressionFunction<'eq', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { - return context === args.value; + fn: (input, args) => { + return input === args.value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index 5e1775940c86a..88a24186d6044 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter, ExpressionFunction } from '../../../types'; +import { Filter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,7 +13,7 @@ interface Arguments { filterGroup: string; } -export function exactly(): ExpressionFunction<'exactly', Filter, Arguments, Filter> { +export function exactly(): ExpressionFunctionDefinition<'exactly', Filter, Arguments, Filter> { const { help, args: argHelp } = getFunctionHelp().exactly; return { @@ -21,9 +21,7 @@ export function exactly(): ExpressionFunction<'exactly', Filter, Arguments, Filt aliases: [], type: 'filter', help, - context: { - types: ['filter'], - }, + inputTypes: ['filter'], args: { column: { types: ['string'], @@ -42,7 +40,7 @@ export function exactly(): ExpressionFunction<'exactly', Filter, Arguments, Filt help: argHelp.filterGroup, }, }, - fn: (context, args) => { + fn: (input, args) => { const { value, column } = args; const filter = { @@ -52,7 +50,7 @@ export function exactly(): ExpressionFunction<'exactly', Filter, Arguments, Filt and: [], }; - return { ...context, and: [...context.and, filter] }; + return { ...input, and: [...input.and, filter] }; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts index 5c9502cd51dbf..17d5211588238 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, ExpressionFunction } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { fn: (datatable: Datatable) => Promise; } -export function filterrows(): ExpressionFunction< +export function filterrows(): ExpressionFunctionDefinition< 'filterrows', Datatable, Arguments, @@ -23,10 +23,8 @@ export function filterrows(): ExpressionFunction< name: 'filterrows', aliases: [], type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { fn: { resolve: false, @@ -36,20 +34,20 @@ export function filterrows(): ExpressionFunction< help: argHelp.fn, }, }, - fn(context, { fn }) { - const checks = context.rows.map(row => + fn(input, { fn }) { + const checks = input.rows.map(row => fn({ - ...context, + ...input, rows: [row], }) ); return Promise.all(checks) - .then(results => context.rows.filter((row, i) => results[i])) + .then(results => input.rows.filter((row, i) => results[i])) .then( rows => ({ - ...context, + ...input, rows, } as Datatable) ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatdate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatdate.ts index 921f14f1e1634..ba892ef3dae44 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatdate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatdate.ts @@ -5,23 +5,26 @@ */ import moment from 'moment'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; export interface Arguments { format: string; } -export function formatdate(): ExpressionFunction<'formatdate', number | string, Arguments, string> { +export function formatdate(): ExpressionFunctionDefinition< + 'formatdate', + number | string, + Arguments, + string +> { const { help, args: argHelp } = getFunctionHelp().formatdate; return { name: 'formatdate', type: 'string', + inputTypes: ['number', 'string'], help, - context: { - types: ['number', 'string'], - }, args: { format: { aliases: ['_'], @@ -30,11 +33,11 @@ export function formatdate(): ExpressionFunction<'formatdate', number | string, help: argHelp.format, }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.format) { - return moment.utc(new Date(context)).toISOString(); + return moment.utc(new Date(input)).toISOString(); } - return moment.utc(new Date(context)).format(args.format); + return moment.utc(new Date(input)).format(args.format); }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.ts index 38040513a47d1..0584b31b7c8a4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/formatnumber.ts @@ -5,23 +5,26 @@ */ import numeral from '@elastic/numeral'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; export interface Arguments { format: string; } -export function formatnumber(): ExpressionFunction<'formatnumber', number, Arguments, string> { +export function formatnumber(): ExpressionFunctionDefinition< + 'formatnumber', + number, + Arguments, + string +> { const { help, args: argHelp } = getFunctionHelp().formatnumber; return { name: 'formatnumber', type: 'string', help, - context: { - types: ['number'], - }, + inputTypes: ['number'], args: { format: { aliases: ['_'], @@ -30,11 +33,11 @@ export function formatnumber(): ExpressionFunction<'formatnumber', number, Argum required: true, }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.format) { - return String(context); + return String(input); } - return numeral(context).format(args.format); + return numeral(input).format(args.format); }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/getCell.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/getCell.ts index 98e8cc86f29e8..bb435629a578e 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/getCell.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/getCell.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Datatable } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; @@ -12,16 +12,14 @@ interface Arguments { row: number; } -export function getCell(): ExpressionFunction<'getCell', Datatable, Arguments, any> { +export function getCell(): ExpressionFunctionDefinition<'getCell', Datatable, Arguments, any> { const { help, args: argHelp } = getFunctionHelp().getCell; const errors = getFunctionErrors().getCell; return { name: 'getCell', help, - context: { - types: ['datatable'], - }, + inputTypes: ['datatable'], args: { column: { types: ['string'], @@ -35,13 +33,13 @@ export function getCell(): ExpressionFunction<'getCell', Datatable, Arguments, a default: 0, }, }, - fn: (context, args) => { - const row = context.rows[args.row]; + fn: (input, args) => { + const row = input.rows[args.row]; if (!row) { throw errors.rowNotFound(args.row); } - const { column = context.columns[0].name } = args; + const { column = input.columns[0].name } = args; const value = row[column]; if (typeof value === 'undefined') { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gt.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gt.ts index 88ff04161222d..b4c6bce5bd31c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gt.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gt.ts @@ -3,24 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; -type Context = number | string; +type Input = number | string; interface Arguments { - value: Context; + value: Input; } -export function gt(): ExpressionFunction<'gt', Context, Arguments, boolean> { +export function gt(): ExpressionFunctionDefinition<'gt', Input, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().gt; return { name: 'gt', type: 'boolean', - context: { - types: ['number', 'string'], - }, + inputTypes: ['number', 'string'], help, args: { value: { @@ -30,14 +28,14 @@ export function gt(): ExpressionFunction<'gt', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { + fn: (input, args) => { const { value } = args; - if (typeof context !== typeof value) { + if (typeof input !== typeof value) { return false; } - return context > value; + return input > value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gte.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gte.ts index c2c9fe2f476fc..3ddab57b5429b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gte.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/gte.ts @@ -3,24 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; -type Context = number | string; +type Input = number | string; interface Arguments { - value: Context; + value: Input; } -export function gte(): ExpressionFunction<'gte', Context, Arguments, boolean> { +export function gte(): ExpressionFunctionDefinition<'gte', Input, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().gte; return { name: 'gte', type: 'boolean', - context: { - types: ['number', 'string'], - }, + inputTypes: ['number', 'string'], help, args: { value: { @@ -30,14 +28,14 @@ export function gte(): ExpressionFunction<'gte', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { + fn: (input, args) => { const { value } = args; - if (typeof context !== typeof value) { + if (typeof input !== typeof value) { return false; } - return context >= value; + return input >= value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/head.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/head.ts index b16e383de6467..b91db30c2535b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/head.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/head.ts @@ -5,24 +5,22 @@ */ import { take } from 'lodash'; -import { Datatable, ExpressionFunction } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { count: number; } -export function head(): ExpressionFunction<'head', Datatable, Arguments, Datatable> { +export function head(): ExpressionFunctionDefinition<'head', Datatable, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().head; return { name: 'head', aliases: [], type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { count: { aliases: ['_'], @@ -31,9 +29,9 @@ export function head(): ExpressionFunction<'head', Datatable, Arguments, Datatab default: 1, }, }, - fn: (context, args) => ({ - ...context, - rows: take(context.rows, args.count), + fn: (input, args) => ({ + ...input, + rows: take(input.rows, args.count), }), }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/if.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/if.ts index 1be8777a98555..6b9464843fca4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/if.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/if.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -12,7 +12,7 @@ interface Arguments { else: () => Promise; } -export function ifFn(): ExpressionFunction<'if', any, Arguments, any> { +export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, unknown> { const { help, args: argHelp } = getFunctionHelp().if; return { @@ -33,15 +33,15 @@ export function ifFn(): ExpressionFunction<'if', any, Arguments, any> { help: argHelp.else, }, }, - fn: async (context, args) => { + fn: async (input, args) => { if (args.condition) { if (typeof args.then === 'undefined') { - return context; + return input; } return await args.then(); } else { if (typeof args.else === 'undefined') { - return context; + return input; } return await args.else(); } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/image.ts index d21e0bb360ab0..c43ff6373ea0f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/image.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/image.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; // @ts-ignore untyped local @@ -28,7 +28,7 @@ interface Return { dataurl: string; } -export function image(): ExpressionFunction<'image', null, Arguments, Return> { +export function image(): ExpressionFunctionDefinition<'image', null, Arguments, Return> { const { help, args: argHelp } = getFunctionHelp().image; const errors = getFunctionErrors().image; @@ -36,10 +36,8 @@ export function image(): ExpressionFunction<'image', null, Arguments, Return> { name: 'image', aliases: [], type: 'image', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { dataurl: { // This was accepting dataurl, but there was no facility in fn for checking type and handling a dataurl type. @@ -55,7 +53,7 @@ export function image(): ExpressionFunction<'image', null, Arguments, Return> { options: Object.values(ImageMode), }, }, - fn: (_context, { dataurl, mode }) => { + fn: (input, { dataurl, mode }) => { if (!mode || !Object.values(ImageMode).includes(mode)) { throw errors.invalidImageMode(); } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/join_rows.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/join_rows.ts index 687b95188a98c..7f8a7b525180c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/join_rows.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/join_rows.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, ExpressionFunction } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -23,16 +23,14 @@ const escapeString = (data: string, quotechar: string): string => { } }; -export function joinRows(): ExpressionFunction<'joinRows', Datatable, Arguments, string> { +export function joinRows(): ExpressionFunctionDefinition<'joinRows', Datatable, Arguments, string> { const { help, args: argHelp } = getFunctionHelp().joinRows; const errors = getFunctionErrors().joinRows; return { name: 'joinRows', type: 'string', help, - context: { - types: ['datatable'], - }, + inputTypes: ['datatable'], args: { column: { aliases: ['_'], @@ -57,14 +55,14 @@ export function joinRows(): ExpressionFunction<'joinRows', Datatable, Arguments, default: ',', }, }, - fn: (context, { column, separator, quote, distinct }) => { - const columnMatch = context.columns.find(col => col.name === column); + fn: (input, { column, separator, quote, distinct }) => { + const columnMatch = input.columns.find(col => col.name === column); if (!columnMatch) { throw errors.columnNotFound(column); } - return context.rows + return input.rows .reduce((acc, row) => { const value = row[column]; if (distinct && acc.includes(value)) return acc; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lt.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lt.ts index c6ca30e7e5e91..6c51ea9705669 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lt.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lt.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; type Context = number | string; @@ -12,15 +12,13 @@ interface Arguments { value: Context; } -export function lt(): ExpressionFunction<'lt', Context, Arguments, boolean> { +export function lt(): ExpressionFunctionDefinition<'lt', Context, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().lt; return { name: 'lt', type: 'boolean', - context: { - types: ['number', 'string'], - }, + inputTypes: ['number', 'string'], help, args: { value: { @@ -30,14 +28,14 @@ export function lt(): ExpressionFunction<'lt', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { + fn: (input, args) => { const { value } = args; - if (typeof context !== typeof value) { + if (typeof input !== typeof value) { return false; } - return context < value; + return input < value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lte.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lte.ts index b976600aaab94..470e4f5f08cf8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lte.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/lte.ts @@ -3,24 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; import { getFunctionHelp } from '../../../i18n'; -type Context = number | string; +type Input = number | string; interface Arguments { - value: Context; + value: Input; } -export function lte(): ExpressionFunction<'lte', Context, Arguments, boolean> { +export function lte(): ExpressionFunctionDefinition<'lte', Input, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().lte; return { name: 'lte', type: 'boolean', - context: { - types: ['number', 'string'], - }, + inputTypes: ['number', 'string'], help, args: { value: { @@ -30,14 +28,14 @@ export function lte(): ExpressionFunction<'lte', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { + fn: (input, args) => { const { value } = args; - if (typeof context !== typeof value) { + if (typeof input !== typeof value) { return false; } - return context <= value; + return input <= value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts index 701322066f100..d8b15a65252e6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datatable, ExpressionFunction, getType } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -12,7 +12,7 @@ interface Arguments { expression: (datatable: Datatable) => Promise; } -export function mapColumn(): ExpressionFunction< +export function mapColumn(): ExpressionFunctionDefinition< 'mapColumn', Datatable, Arguments, @@ -24,10 +24,8 @@ export function mapColumn(): ExpressionFunction< name: 'mapColumn', aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file. type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { name: { types: ['string'], @@ -43,11 +41,11 @@ export function mapColumn(): ExpressionFunction< required: true, }, }, - fn: (context, args) => { + fn: (input, args) => { const expression = args.expression || (() => Promise.resolve(null)); - const columns = [...context.columns]; - const rowPromises = context.rows.map(row => { + const columns = [...input.columns]; + const rowPromises = input.rows.map(row => { return expression({ type: 'datatable', columns, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts index 21f9e9fe3148d..8ec2b7d7d3dc3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n/functions'; import { MapCenter } from '../../../types'; @@ -14,15 +14,13 @@ interface Args { zoom: number; } -export function mapCenter(): ExpressionFunction<'mapCenter', null, Args, MapCenter> { +export function mapCenter(): ExpressionFunctionDefinition<'mapCenter', null, Args, MapCenter> { const { help, args: argHelp } = getFunctionHelp().mapCenter; return { name: 'mapCenter', help, type: 'mapCenter', - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { lat: { types: ['number'], @@ -40,7 +38,7 @@ export function mapCenter(): ExpressionFunction<'mapCenter', null, Args, MapCent help: argHelp.zoom, }, }, - fn: (context, args) => { + fn: (input, args) => { return { type: 'mapCenter', ...args, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts index 8fcdf00a7f8d6..dfbb37be0797c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -8,26 +8,24 @@ import { evaluate } from 'tinymath'; // @ts-ignore untyped local import { pivotObjectArray } from '../../../common/lib/pivot_object_array'; -import { Datatable, isDatatable, ExpressionFunction } from '../../../types'; +import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { expression: string; } -type Context = number | Datatable; +type Input = number | Datatable; -export function math(): ExpressionFunction<'math', Context, Arguments, number> { +export function math(): ExpressionFunctionDefinition<'math', Input, Arguments, number> { const { help, args: argHelp } = getFunctionHelp().math; const errors = getFunctionErrors().math; return { name: 'math', type: 'number', + inputTypes: ['number', 'datatable'], help, - context: { - types: ['number', 'datatable'], - }, args: { expression: { aliases: ['_'], @@ -35,19 +33,19 @@ export function math(): ExpressionFunction<'math', Context, Arguments, number> { help: argHelp.expression, }, }, - fn: (context, args) => { + fn: (input, args) => { const { expression } = args; if (!expression || expression.trim() === '') { throw errors.emptyExpression(); } - const mathContext = isDatatable(context) + const mathContext = isDatatable(input) ? pivotObjectArray( - context.rows, - context.columns.map(col => col.name) + input.rows, + input.columns.map(col => col.name) ) - : { value: context }; + : { value: input }; try { const result = evaluate(expression, mathContext); @@ -62,7 +60,7 @@ export function math(): ExpressionFunction<'math', Context, Arguments, number> { } return result; } catch (e) { - if (isDatatable(context) && context.rows.length === 0) { + if (isDatatable(input) && input.rows.length === 0) { throw errors.emptyDatatable(); } else { throw e; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts index 597e8dd731515..6aab1a7dfb99b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts @@ -5,10 +5,10 @@ */ import { openSans } from '../../../common/lib/fonts'; -import { Render, Style, ExpressionFunction } from '../../../types'; +import { Render, Style, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -type Context = number | string | null; +type Input = number | string | null; interface Arguments { label: string; @@ -17,17 +17,20 @@ interface Arguments { labelFont: Style; } -export function metric(): ExpressionFunction<'metric', Context, Arguments, Render> { +export function metric(): ExpressionFunctionDefinition< + 'metric', + Input, + Arguments, + Render +> { const { help, args: argHelp } = getFunctionHelp().metric; return { name: 'metric', aliases: [], type: 'render', + inputTypes: ['number', 'string', 'null'], help, - context: { - types: ['number', 'string', 'null'], - }, args: { label: { types: ['string'], @@ -51,12 +54,12 @@ export function metric(): ExpressionFunction<'metric', Context, Arguments, Rende help: argHelp.metricFormat, }, }, - fn: (context, { label, labelFont, metricFont, metricFormat }) => { + fn: (input, { label, labelFont, metricFont, metricFormat }) => { return { type: 'render', as: 'metric', value: { - metric: context === null ? '?' : context, + metric: input === null ? '?' : input, label, labelFont, metricFont, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/neq.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/neq.ts index f9026453d340b..4066a35ea41f2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/neq.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/neq.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; -type Context = boolean | number | string | null; +type Input = boolean | number | string | null; interface Arguments { - value: Context; + value: Input; } -export function neq(): ExpressionFunction<'neq', Context, Arguments, boolean> { +export function neq(): ExpressionFunctionDefinition<'neq', Input, Arguments, boolean> { const { help, args: argHelp } = getFunctionHelp().neq; return { @@ -28,8 +28,8 @@ export function neq(): ExpressionFunction<'neq', Context, Arguments, boolean> { help: argHelp.value, }, }, - fn: (context, args) => { - return context !== args.value; + fn: (input, args) => { + return input !== args.value; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/palette.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/palette.ts index 441dce286cac3..63cd663d2ac4c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/palette.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/palette.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { palettes } from '../../../common/lib/palettes'; import { getFunctionHelp } from '../../../i18n'; @@ -15,23 +15,21 @@ interface Arguments { reverse: boolean; } -interface Return { +interface Output { type: 'palette'; colors: string[]; gradient: boolean; } -export function palette(): ExpressionFunction<'palette', null, Arguments, Return> { +export function palette(): ExpressionFunctionDefinition<'palette', null, Arguments, Output> { const { help, args: argHelp } = getFunctionHelp().palette; return { name: 'palette', aliases: [], type: 'palette', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { color: { aliases: ['_'], @@ -52,7 +50,7 @@ export function palette(): ExpressionFunction<'palette', null, Arguments, Return options: [true, false], }, }, - fn: (_context, args) => { + fn: (input, args) => { const { color, reverse, gradient } = args; const colors = ([] as string[]).concat(color || palettes.paul_tor_14.colors); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index a8250cfebfaeb..36f1bf85b97e7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -19,7 +19,7 @@ import { Render, SeriesStyle, Style, - ExpressionFunction, + ExpressionFunctionDefinition, } from '../../../types'; interface PieSeriesOptions { @@ -77,17 +77,15 @@ interface Arguments { tilt: number; } -export function pie(): ExpressionFunction<'pie', PointSeries, Arguments, Render> { +export function pie(): ExpressionFunctionDefinition<'pie', PointSeries, Arguments, Render> { const { help, args: argHelp } = getFunctionHelp().pie; return { name: 'pie', aliases: [], type: 'render', + inputTypes: ['pointseries'], help, - context: { - types: ['pointseries'], - }, args: { font: { types: ['style'], @@ -136,11 +134,11 @@ export function pie(): ExpressionFunction<'pie', PointSeries, Arguments, Render< help: argHelp.tilt, }, }, - fn: (context, args) => { + fn: (input, args) => { const { tilt, radius, labelRadius, labels, hole, legend, palette, font, seriesStyle } = args; const seriesStyles = keyBy(seriesStyle || [], 'label') || {}; - const data: PieData[] = map(groupBy(context.rows, 'color'), (series, label = '') => { + const data: PieData[] = map(groupBy(input.rows, 'color'), (series, label = '') => { const item: PieData = { label, data: series.map(point => point.size || 1), diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts index 98eab84643da6..34e5d9f600d8d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts @@ -7,7 +7,7 @@ // @ts-ignore no @typed def import keyBy from 'lodash.keyby'; import { groupBy, get, set, map, sortBy } from 'lodash'; -import { ExpressionFunction, Style } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; // @ts-ignore untyped local import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette'; // @ts-ignore untyped local @@ -29,17 +29,15 @@ interface Arguments { yaxis: AxisConfig | boolean; } -export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Render> { +export function plot(): ExpressionFunctionDefinition<'plot', PointSeries, Arguments, Render> { const { help, args: argHelp } = getFunctionHelp().plot; return { name: 'plot', aliases: [], type: 'render', + inputTypes: ['pointseries'], help, - context: { - types: ['pointseries'], - }, args: { defaultStyle: { multi: false, @@ -79,12 +77,12 @@ export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Rende default: true, }, }, - fn: (context, args) => { + fn: (input, args) => { const seriesStyles: { [key: string]: SeriesStyle } = keyBy(args.seriesStyle || [], 'label') || {}; - const sortedRows = sortBy(context.rows, ['x', 'y', 'color', 'size', 'text']); - const ticks = getTickHash(context.columns, sortedRows); + const sortedRows = sortBy(input.rows, ['x', 'y', 'color', 'size', 'text']); + const ticks = getTickHash(input.columns, sortedRows); const font = args.font ? getFontSpec(args.font) : {}; const data = map(groupBy(sortedRows, 'color'), (series, label) => { @@ -104,8 +102,8 @@ export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Rende text?: string; } = {}; - const x = get(context.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x; - const y = get(context.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y; + const x = get(input.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x; + const y = get(input.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y; if (point.size != null) { attrs.size = point.size; @@ -136,7 +134,7 @@ export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Rende }, }; - const result = { + const output = { type: 'render', as: 'plot', value: { @@ -148,12 +146,12 @@ export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Rende legend: getLegendConfig(args.legend, data.length), grid: gridConfig, xaxis: getFlotAxisConfig('x', args.xaxis, { - columns: context.columns, + columns: input.columns, ticks, font, }), yaxis: getFlotAxisConfig('y', args.yaxis, { - columns: context.columns, + columns: input.columns, ticks, font, }), @@ -169,7 +167,7 @@ export function plot(): ExpressionFunction<'plot', PointSeries, Arguments, Rende // TODO: holy hell, why does this work?! the working theory is that some values become undefined // and serializing the result here causes them to be dropped off, and this makes flot react differently. // It's also possible that something else ends up mutating this object, but that seems less likely. - return JSON.parse(JSON.stringify(result)); + return JSON.parse(JSON.stringify(output)); }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/ply.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/ply.ts index 24fe16bd8d24d..391ff20461fb4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/ply.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/ply.ts @@ -5,7 +5,7 @@ */ import { groupBy, flatten, pick, map } from 'lodash'; -import { Datatable, DatatableColumn, ExpressionFunction } from '../../../types'; +import { Datatable, DatatableColumn, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -13,19 +13,17 @@ interface Arguments { expression: Array<(datatable: Datatable) => Promise>; } -type Return = Datatable | Promise; +type Output = Datatable | Promise; -export function ply(): ExpressionFunction<'ply', Datatable, Arguments, Return> { +export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, Output> { const { help, args: argHelp } = getFunctionHelp().ply; const errors = getFunctionErrors().ply; return { name: 'ply', type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { by: { types: ['string'], @@ -40,9 +38,9 @@ export function ply(): ExpressionFunction<'ply', Datatable, Arguments, Return> { help: argHelp.expression, }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args) { - return context; + return input; } let byColumns: DatatableColumn[]; @@ -50,7 +48,7 @@ export function ply(): ExpressionFunction<'ply', Datatable, Arguments, Return> { if (args.by) { byColumns = args.by.map(by => { - const column = context.columns.find(col => col.name === by); + const column = input.columns.find(col => col.name === by); if (!column) { throw errors.columnNotFound(by); @@ -59,14 +57,14 @@ export function ply(): ExpressionFunction<'ply', Datatable, Arguments, Return> { return column; }); - const keyedDatatables = groupBy(context.rows, row => JSON.stringify(pick(row, args.by))); + const keyedDatatables = groupBy(input.rows, row => JSON.stringify(pick(row, args.by))); originalDatatables = Object.values(keyedDatatables).map(rows => ({ - ...context, + ...input, rows, })); } else { - originalDatatables = [context]; + originalDatatables = [input]; } const datatablePromises = originalDatatables.map(originalDatatable => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/progress.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/progress.ts index 399c0acf249d1..6fc1e509cd5e6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/progress.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/progress.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { openSans } from '../../../common/lib/fonts'; -import { Render, Style, ExpressionFunction } from '../../../types'; +import { Render, Style, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; export enum Shape { @@ -31,7 +31,12 @@ interface Arguments { valueWeight: number; } -export function progress(): ExpressionFunction<'progress', number, Arguments, Render> { +export function progress(): ExpressionFunctionDefinition< + 'progress', + number, + Arguments, + Render +> { const { help, args: argHelp } = getFunctionHelp().progress; const errors = getFunctionErrors().progress; @@ -39,10 +44,8 @@ export function progress(): ExpressionFunction<'progress', number, Arguments, Re name: 'progress', aliases: [], type: 'render', + inputTypes: ['number'], help, - context: { - types: ['number'], - }, args: { shape: { aliases: ['_'], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/render.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/render.ts index f181f4ed3e513..da50195480c68 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/render.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/render.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render, ContainerStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; // @ts-ignore unconverted local file @@ -19,17 +19,20 @@ interface Arguments { css: string; containerStyle: ContainerStyleArgument; } -export function render(): ExpressionFunction<'render', Render, Arguments, Render> { +export function render(): ExpressionFunctionDefinition< + 'render', + Render, + Arguments, + Render +> { const { help, args: argHelp } = getFunctionHelp().render; return { name: 'render', aliases: [], type: 'render', + inputTypes: ['render'], help, - context: { - types: ['render'], - }, args: { as: { types: ['string'], @@ -64,10 +67,10 @@ export function render(): ExpressionFunction<'render', Render, Arguments, R default: '{containerStyle}', }, }, - fn: (context, args) => { + fn: (input, args) => { return { - ...context, - as: args.as || context.as, + ...input, + as: args.as || input.as, css: args.css || DEFAULT_ELEMENT_CSS, containerStyle: args.containerStyle, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts index f52dc140f1c8c..f91fd3dfc5522 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; // @ts-ignore .png file @@ -19,7 +19,7 @@ interface Arguments { emptyImage: string | null; } -export function repeatImage(): ExpressionFunction< +export function repeatImage(): ExpressionFunctionDefinition< 'repeatImage', number, Arguments, @@ -31,10 +31,8 @@ export function repeatImage(): ExpressionFunction< name: 'repeatImage', aliases: [], type: 'render', + inputTypes: ['number'], help, - context: { - types: ['number'], - }, args: { emptyImage: { types: ['string', 'null'], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/replace.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/replace.ts index 3cb6d17b7cd4f..70497f39de9a7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/replace.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/replace.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -12,16 +12,14 @@ interface Arguments { flags: string; replacement: string; } -export function replace(): ExpressionFunction<'replace', string, Arguments, string> { +export function replace(): ExpressionFunctionDefinition<'replace', string, Arguments, string> { const { help, args: argHelp } = getFunctionHelp().replace; return { name: 'replace', type: 'string', help, - context: { - types: ['string'], - }, + inputTypes: ['string'], args: { pattern: { aliases: ['_', 'regex'], @@ -40,6 +38,6 @@ export function replace(): ExpressionFunction<'replace', string, Arguments, stri default: '""', }, }, - fn: (context, args) => context.replace(new RegExp(args.pattern, args.flags), args.replacement), + fn: (input, args) => input.replace(new RegExp(args.pattern, args.flags), args.replacement), }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts index 4b327ab91af41..d961227a302b8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition, ExpressionValueRender } from 'src/plugins/expressions'; // @ts-ignore untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; // @ts-ignore .png file import { elasticOutline } from '../../lib/elastic_outline'; -import { Render } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; export enum Origin { @@ -25,11 +24,11 @@ interface Arguments { origin: Origin; } -export function revealImage(): ExpressionFunction< +export function revealImage(): ExpressionFunctionDefinition< 'revealImage', number, Arguments, - Render + ExpressionValueRender > { const { help, args: argHelp } = getFunctionHelp().revealImage; const errors = getFunctionErrors().revealImage; @@ -38,10 +37,8 @@ export function revealImage(): ExpressionFunction< name: 'revealImage', aliases: [], type: 'render', + inputTypes: ['number'], help, - context: { - types: ['number'], - }, args: { image: { types: ['string', 'null'], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rounddate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rounddate.ts index 275484458384e..a215f545fd531 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rounddate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rounddate.ts @@ -5,23 +5,21 @@ */ import moment from 'moment'; -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; export interface Arguments { format: string; } -export function rounddate(): ExpressionFunction<'rounddate', number, Arguments, number> { +export function rounddate(): ExpressionFunctionDefinition<'rounddate', number, Arguments, number> { const { help, args: argHelp } = getFunctionHelp().rounddate; return { name: 'rounddate', type: 'number', help, - context: { - types: ['number'], - }, + inputTypes: ['number'], args: { format: { aliases: ['_'], @@ -29,11 +27,11 @@ export function rounddate(): ExpressionFunction<'rounddate', number, Arguments, help: argHelp.format, }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.format) { - return context; + return input; } - return moment.utc(moment.utc(context).format(args.format), args.format).valueOf(); + return moment.utc(moment.utc(input).format(args.format), args.format).valueOf(); }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rowCount.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rowCount.ts index 9104343d7afe8..d1027f784c9a9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rowCount.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/rowCount.ts @@ -4,22 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Datatable } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -export function rowCount(): ExpressionFunction<'rowCount', Datatable, {}, number> { +export function rowCount(): ExpressionFunctionDefinition<'rowCount', Datatable, {}, number> { const { help } = getFunctionHelp().rowCount; return { name: 'rowCount', aliases: [], type: 'number', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: {}, - fn: context => context.rows.length, + fn: input => input.rows.length, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts index 5b95886faa13d..cf0c76be4580d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts @@ -31,13 +31,13 @@ describe('savedMap', () => { }; it('accepts null context', () => { - const expression = fn(null, args, {}); + const expression = fn(null, args, {} as any); expect(expression.input.filters).toEqual([]); }); it('accepts filter context', () => { - const expression = fn(filterContext, args, {}); + const expression = fn(filterContext, args, {} as any); const embeddableFilters = getQueryFilters(filterContext.and); expect(expression.input.filters).toEqual(embeddableFilters); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index b6d88c06ed06d..0d4616635d5c7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { TimeRange } from 'src/plugins/data/public'; import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { getQueryFilters } from '../../../server/lib/build_embeddable_filters'; @@ -50,9 +50,14 @@ const defaultTimeRange = { to: 'now', }; -type Return = EmbeddableExpression; +type Output = EmbeddableExpression; -export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Arguments, Return> { +export function savedMap(): ExpressionFunctionDefinition< + 'savedMap', + Filter | null, + Arguments, + Output +> { const { help, args: argHelp } = getFunctionHelp().savedMap; return { name: 'savedMap', @@ -86,8 +91,8 @@ export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Argume }, }, type: EmbeddableExpressionType, - fn: (context, args) => { - const filters = context ? context.and : []; + fn: (input, args) => { + const filters = input ? input.and : []; const center = args.center ? { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts index 9e5d4b2dd31a1..294d6124c7e33 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts @@ -27,14 +27,14 @@ describe('savedSearch', () => { }; it('accepts null context', () => { - const expression = fn(null, args, {}); + const expression = fn(null, args, {} as any); expect(expression.input.filters).toEqual([]); expect(expression.input.timeRange).toBeUndefined(); }); it('accepts filter context', () => { - const expression = fn(filterContext, args, {}); + const expression = fn(filterContext, args, {} as any); const embeddableFilters = buildEmbeddableFilters(filterContext.and); expect(expression.input.filters).toEqual(embeddableFilters.filters); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts index 4895571115898..a351bcb46cdd3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { SearchInput } from 'src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable'; import { EmbeddableTypes, @@ -20,9 +20,14 @@ interface Arguments { id: string; } -type Return = EmbeddableExpression & { id: SearchInput['id'] }>; +type Output = EmbeddableExpression & { id: SearchInput['id'] }>; -export function savedSearch(): ExpressionFunction<'savedSearch', Filter | null, Arguments, Return> { +export function savedSearch(): ExpressionFunctionDefinition< + 'savedSearch', + Filter | null, + Arguments, + Output +> { const { help, args: argHelp } = getFunctionHelp().savedSearch; return { name: 'savedSearch', @@ -35,8 +40,8 @@ export function savedSearch(): ExpressionFunction<'savedSearch', Filter | null, }, }, type: EmbeddableExpressionType, - fn: (context, { id }) => { - const filters = context ? context.and : []; + fn: (input, { id }) => { + const filters = input ? input.and : []; return { type: EmbeddableExpressionType, input: { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts index 965491272cef8..49b4b77de763b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts @@ -27,14 +27,14 @@ describe('savedVisualization', () => { }; it('accepts null context', () => { - const expression = fn(null, args, {}); + const expression = fn(null, args, {} as any); expect(expression.input.filters).toEqual([]); expect(expression.input.timeRange).toBeUndefined(); }); it('accepts filter context', () => { - const expression = fn(filterContext, args, {}); + const expression = fn(filterContext, args, {} as any); const embeddableFilters = buildEmbeddableFilters(filterContext.and); expect(expression.input.filters).toEqual(embeddableFilters.filters); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index d3b1bbe31c715..737db985f99d0 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; import { VisualizeInput } from 'src/legacy/core_plugins/visualizations/public/embeddable'; import { EmbeddableTypes, @@ -19,13 +19,13 @@ interface Arguments { id: string; } -type Return = EmbeddableExpression; +type Output = EmbeddableExpression; -export function savedVisualization(): ExpressionFunction< +export function savedVisualization(): ExpressionFunctionDefinition< 'savedVisualization', Filter | null, Arguments, - Return + Output > { const { help, args: argHelp } = getFunctionHelp().savedVisualization; return { @@ -39,8 +39,8 @@ export function savedVisualization(): ExpressionFunction< }, }, type: EmbeddableExpressionType, - fn: (context, { id }) => { - const filters = context ? context.and : []; + fn: (input, { id }) => { + const filters = input ? input.and : []; return { type: EmbeddableExpressionType, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/seriesStyle.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/seriesStyle.ts index 4ae57878e36fe..6c80eb02f2a8b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/seriesStyle.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/seriesStyle.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; const name = 'seriesStyle'; @@ -20,20 +20,23 @@ interface Arguments { stack: number | null; } -interface Return extends Arguments { +interface Output extends Arguments { type: 'seriesStyle'; } -export function seriesStyle(): ExpressionFunction<'seriesStyle', null, Arguments, Return> { +export function seriesStyle(): ExpressionFunctionDefinition< + 'seriesStyle', + null, + Arguments, + Output +> { const { help, args: argHelp } = getFunctionHelp().seriesStyle; return { name, help, type: 'seriesStyle', - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { bars: { types: ['number'], @@ -71,6 +74,6 @@ export function seriesStyle(): ExpressionFunction<'seriesStyle', null, Arguments help: argHelp.stack, }, }, - fn: (_context, args) => ({ type: name, ...args }), + fn: (input, args) => ({ type: name, ...args }), }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/shape.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/shape.ts index a96d39f9914ec..a3fedebd36cfe 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/shape.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/shape.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common/types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; import { getFunctionHelp } from '../../../i18n'; export enum Shape { @@ -34,21 +34,19 @@ interface Arguments { maintainAspect: boolean; } -interface Return extends Arguments { +interface Output extends Arguments { type: 'shape'; } -export function shape(): ExpressionFunction<'shape', null, Arguments, Return> { +export function shape(): ExpressionFunctionDefinition<'shape', null, Arguments, Output> { const { help, args: argHelp } = getFunctionHelp().shape; return { name: 'shape', aliases: [], type: 'shape', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { shape: { types: ['string'], @@ -80,7 +78,7 @@ export function shape(): ExpressionFunction<'shape', null, Arguments, Return> { options: [true, false], }, }, - fn: (_context, args) => ({ + fn: (input, args) => ({ type: 'shape', ...args, }), diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/sort.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/sort.ts index a7dcfff87631f..40d7dce844748 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/sort.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/sort.ts @@ -5,7 +5,7 @@ */ import { sortBy } from 'lodash'; -import { ExpressionFunction, Datatable } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,16 +13,14 @@ interface Arguments { reverse: boolean; } -export function sort(): ExpressionFunction<'sort', Datatable, Arguments, Datatable> { +export function sort(): ExpressionFunctionDefinition<'sort', Datatable, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().sort; return { name: 'sort', type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { by: { types: ['string'], @@ -37,12 +35,12 @@ export function sort(): ExpressionFunction<'sort', Datatable, Arguments, Datatab default: false, }, }, - fn: (context, args) => { - const column = args.by || context.columns[0].name; + fn: (input, args) => { + const column = args.by || input.columns[0].name; return { - ...context, - rows: args.reverse ? sortBy(context.rows, column).reverse() : sortBy(context.rows, column), + ...input, + rows: args.reverse ? sortBy(input.rows, column).reverse() : sortBy(input.rows, column), }; }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts index 3cf879d2b67a4..2354f2405de76 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts @@ -6,7 +6,7 @@ // @ts-ignore untyped Elastic library import { getType } from '@kbn/interpreter/common'; -import { ExpressionFunction, Datatable } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -14,7 +14,7 @@ interface Arguments { value: string | number | boolean | null; } -export function staticColumn(): ExpressionFunction< +export function staticColumn(): ExpressionFunctionDefinition< 'staticColumn', Datatable, Arguments, @@ -25,10 +25,8 @@ export function staticColumn(): ExpressionFunction< return { name: 'staticColumn', type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { name: { types: ['string'], @@ -42,10 +40,10 @@ export function staticColumn(): ExpressionFunction< default: null, }, }, - fn: (context, args) => { - const rows = context.rows.map(row => ({ ...row, [args.name]: args.value })); + fn: (input, args) => { + const rows = input.rows.map(row => ({ ...row, [args.name]: args.value })); const type = getType(args.value); - const columns = [...context.columns]; + const columns = [...input.columns]; const existingColumnIndex = columns.findIndex(({ name }) => name === args.name); const newColumn = { name: args.name, type }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/string.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/string.ts index e1fc567ad009e..c7cee0da2a674 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/string.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/string.ts @@ -3,21 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { value: Array; } -export function string(): ExpressionFunction<'string', null, Arguments, string> { +export function string(): ExpressionFunctionDefinition<'string', null, Arguments, string> { const { help, args: argHelp } = getFunctionHelp().string; return { name: 'string', - context: { - types: ['null'], - }, + inputTypes: ['null'], aliases: [], type: 'string', help, @@ -29,6 +27,6 @@ export function string(): ExpressionFunction<'string', null, Arguments, string> help: argHelp.value, }, }, - fn: (_context, args) => args.value.join(''), + fn: (input, args) => args.value.join(''), }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index f6d396361a1ae..bb70bec561a11 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Case } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; @@ -13,7 +13,7 @@ interface Arguments { default: () => any; } -export function switchFn(): ExpressionFunction<'switch', any, Arguments, any> { +export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Arguments, unknown> { const { help, args: argHelp } = getFunctionHelp().switch; return { @@ -33,7 +33,7 @@ export function switchFn(): ExpressionFunction<'switch', any, Arguments, any> { help: argHelp.default, }, }, - fn: async (context, args) => { + fn: async (input, args) => { const cases = args.case || []; for (let i = 0; i < cases.length; i++) { @@ -48,7 +48,7 @@ export function switchFn(): ExpressionFunction<'switch', any, Arguments, any> { return await args.default(); } - return context; + return input; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/table.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/table.ts index 45612474fbe53..689f3f969d1c8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/table.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/table.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Datatable, Render, Style } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; @@ -15,17 +15,20 @@ interface Arguments { showHeader: boolean; } -export function table(): ExpressionFunction<'table', Datatable, Arguments, Render> { +export function table(): ExpressionFunctionDefinition< + 'table', + Datatable, + Arguments, + Render +> { const { help, args: argHelp } = getFunctionHelp().table; return { name: 'table', aliases: [], type: 'render', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { font: { types: ['style'], @@ -50,12 +53,12 @@ export function table(): ExpressionFunction<'table', Datatable, Arguments, Rende options: [true, false], }, }, - fn: (context, args) => { + fn: (input, args) => { return { type: 'render', as: 'table', value: { - datatable: context, + datatable: input, ...args, }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/tail.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/tail.ts index bd2fc03e8230d..5105beb586f72 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/tail.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/tail.ts @@ -5,24 +5,22 @@ */ import { takeRight } from 'lodash'; -import { Datatable, ExpressionFunction } from '../../../types'; +import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { count: number; } -export function tail(): ExpressionFunction<'tail', Datatable, Arguments, Datatable> { +export function tail(): ExpressionFunctionDefinition<'tail', Datatable, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().tail; return { name: 'tail', aliases: [], type: 'datatable', + inputTypes: ['datatable'], help, - context: { - types: ['datatable'], - }, args: { count: { aliases: ['_'], @@ -30,9 +28,9 @@ export function tail(): ExpressionFunction<'tail', Datatable, Arguments, Datatab help: argHelp.count, }, }, - fn: (context, args) => ({ - ...context, - rows: takeRight(context.rows, args.count), + fn: (input, args) => ({ + ...input, + rows: takeRight(input.rows, args.count), }), }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts index 716026279ccea..8b311d9be2bbf 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n/functions'; import { TimeRange } from '../../../types'; @@ -13,15 +13,13 @@ interface Args { to: string; } -export function timerange(): ExpressionFunction<'timerange', null, Args, TimeRange> { +export function timerange(): ExpressionFunctionDefinition<'timerange', null, Args, TimeRange> { const { help, args: argHelp } = getFunctionHelp().timerange; return { name: 'timerange', help, type: 'timerange', - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { from: { types: ['string'], @@ -34,7 +32,7 @@ export function timerange(): ExpressionFunction<'timerange', null, Args, TimeRan help: argHelp.to, }, }, - fn: (context, args) => { + fn: (input, args) => { return { type: 'timerange', ...args, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index 92d2183caa298..8afa6eb04ad69 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { Filter, ExpressionFunction } from '../../../types'; +import { Filter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -15,7 +15,12 @@ interface Arguments { filterGroup: string; } -export function timefilter(): ExpressionFunction<'timefilter', Filter, Arguments, Filter> { +export function timefilter(): ExpressionFunctionDefinition< + 'timefilter', + Filter, + Arguments, + Filter +> { const { help, args: argHelp } = getFunctionHelp().timefilter; const errors = getFunctionErrors().timefilter; @@ -23,9 +28,7 @@ export function timefilter(): ExpressionFunction<'timefilter', Filter, Arguments name: 'timefilter', aliases: [], type: 'filter', - context: { - types: ['filter'], - }, + inputTypes: ['filter'], help, args: { column: { @@ -49,9 +52,9 @@ export function timefilter(): ExpressionFunction<'timefilter', Filter, Arguments help: 'The group name for the filter', }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.from && !args.to) { - return context; + return input; } const { from, to, column } = args; @@ -79,7 +82,7 @@ export function timefilter(): ExpressionFunction<'timefilter', Filter, Arguments (filter as any).from = parseAndValidate(from); } - return { ...context, and: [...context.and, filter] }; + return { ...input, and: [...input.and, filter] }; }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts index 8e796e47c7c0f..5b6c0cb97b0fd 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; @@ -13,7 +13,7 @@ interface Arguments { compact: boolean; filterGroup: string; } -export function timefilterControl(): ExpressionFunction< +export function timefilterControl(): ExpressionFunctionDefinition< 'timefilterControl', null, Arguments, @@ -25,9 +25,7 @@ export function timefilterControl(): ExpressionFunction< name: 'timefilterControl', aliases: [], type: 'render', - context: { - types: ['null'], - }, + inputTypes: ['null'], help, args: { column: { @@ -47,7 +45,7 @@ export function timefilterControl(): ExpressionFunction< help: argHelp.filterGroup, }, }, - fn: (_context, args) => { + fn: (input, args) => { return { type: 'render', as: 'time_filter', diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts index a592127e23948..94b2d5228665b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts @@ -15,25 +15,28 @@ const nullFilter = { }; const fn = demodata().fn; +const context = {} as any; describe('demodata', () => { it('ci, different object references', () => { - const ci1 = fn(nullFilter, { type: 'ci' }, {}); - const ci2 = fn(nullFilter, { type: 'ci' }, {}); + const ci1 = fn(nullFilter, { type: 'ci' }, context); + const ci2 = fn(nullFilter, { type: 'ci' }, context); expect(ci1).not.toBe(ci2); expect(ci1.rows).not.toBe(ci2.rows); expect(ci1.rows[0]).not.toBe(ci2.rows[0]); }); + it('shirts, different object references', () => { - const shirts1 = fn(nullFilter, { type: 'shirts' }, {}); - const shirts2 = fn(nullFilter, { type: 'shirts' }, {}); + const shirts1 = fn(nullFilter, { type: 'shirts' }, context); + const shirts2 = fn(nullFilter, { type: 'shirts' }, context); expect(shirts1).not.toBe(shirts2); expect(shirts1.rows).not.toBe(shirts2.rows); expect(shirts1.rows[0]).not.toBe(shirts2.rows[0]); }); + it('invalid set', () => { expect(() => { - fn(nullFilter, { type: 'foo' }, {}); + fn(nullFilter, { type: 'foo' }, context); }).toThrowError("Invalid data set: 'foo', use 'ci' or 'shirts'."); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index a803ca766d861..826c49d328f21 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -5,7 +5,7 @@ */ import { sortBy } from 'lodash'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore unconverted lib file import { queryDatatable } from '../../../../common/lib/datatable/query'; import { DemoRows, getDemoRows } from './get_demo_rows'; @@ -16,17 +16,17 @@ interface Arguments { type: string; } -export function demodata(): ExpressionFunction<'demodata', Filter, Arguments, Datatable> { +export function demodata(): ExpressionFunctionDefinition<'demodata', Filter, Arguments, Datatable> { const { help, args: argHelp } = getFunctionHelp().demodata; return { name: 'demodata', aliases: [], type: 'datatable', - help, context: { types: ['filter'], }, + help, args: { type: { types: ['string'], @@ -36,7 +36,7 @@ export function demodata(): ExpressionFunction<'demodata', Filter, Arguments, Da options: ['ci', 'shirts'], }, }, - fn: (context, args) => { + fn: (input, args) => { const demoRows = getDemoRows(args.type); let set = {} as { columns: DatatableColumn[]; rows: DatatableRow[] }; @@ -76,7 +76,7 @@ export function demodata(): ExpressionFunction<'demodata', Filter, Arguments, Da columns, rows, }, - context + input ); }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts index ad572f15b9870..ffb8bb4f3e2a7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction, Filter } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition, Filter } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { buildESRequest } from '../../../server/lib/build_es_request'; import { getFunctionHelp } from '../../../i18n'; @@ -14,16 +14,16 @@ interface Arguments { query: string; } -export function escount(): ExpressionFunction<'escount', Filter, Arguments, any> { +export function escount(): ExpressionFunctionDefinition<'escount', Filter, Arguments, any> { const { help, args: argHelp } = getFunctionHelp().escount; return { name: 'escount', type: 'number', - help, context: { types: ['filter'], }, + help, args: { query: { types: ['string'], @@ -37,8 +37,8 @@ export function escount(): ExpressionFunction<'escount', Filter, Arguments, any> help: argHelp.index, }, }, - fn: (context, args, handlers) => { - context.and = context.and.concat([ + fn: (input, args, handlers) => { + input.and = input.and.concat([ { type: 'luceneQueryString', query: args.query, @@ -57,10 +57,10 @@ export function escount(): ExpressionFunction<'escount', Filter, Arguments, any> }, }, }, - context + input ); - return handlers + return ((handlers as any) as { elasticsearchClient: any }) .elasticsearchClient('count', esRequest) .then((resp: { count: number }) => resp.count); }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index ddd39197eb256..5bff06bb3933b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -5,7 +5,7 @@ */ import squel from 'squel'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; import { Filter } from '../../../types'; @@ -20,16 +20,16 @@ interface Arguments { count: number; } -export function esdocs(): ExpressionFunction<'esdocs', Filter, Arguments, any> { +export function esdocs(): ExpressionFunctionDefinition<'esdocs', Filter, Arguments, any> { const { help, args: argHelp } = getFunctionHelp().esdocs; return { name: 'esdocs', type: 'datatable', - help, context: { types: ['filter'], }, + help, args: { query: { types: ['string'], @@ -62,10 +62,10 @@ export function esdocs(): ExpressionFunction<'esdocs', Filter, Arguments, any> { help: argHelp.sort, }, }, - fn: (context, args, handlers) => { + fn: (input, args, context) => { const { count, index, fields, sort } = args; - context.and = context.and.concat([ + input.and = input.and.concat([ { type: 'luceneQueryString', query: args.query, @@ -96,10 +96,10 @@ export function esdocs(): ExpressionFunction<'esdocs', Filter, Arguments, any> { } } - return queryEsSQL(handlers.elasticsearchClient, { + return queryEsSQL(((context as any) as { elasticsearchClient: any }).elasticsearchClient, { count, query: query.toString(), - filter: context.and, + filter: input.and, }); }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 2106a4e9877e6..cdb6b5af82015 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; import { Filter } from '../../../types'; @@ -16,16 +16,16 @@ interface Arguments { timezone: string; } -export function essql(): ExpressionFunction<'essql', Filter, Arguments, any> { +export function essql(): ExpressionFunctionDefinition<'essql', Filter, Arguments, any> { const { help, args: argHelp } = getFunctionHelp().essql; return { name: 'essql', type: 'datatable', - help, context: { types: ['filter'], }, + help, args: { query: { aliases: ['_', 'q'], @@ -44,7 +44,11 @@ export function essql(): ExpressionFunction<'essql', Filter, Arguments, any> { help: argHelp.timezone, }, }, - fn: (context, args, handlers) => - queryEsSQL(handlers.elasticsearchClient, { ...args, filter: context.and }), + fn: (input, args, context) => { + return queryEsSQL(((context as any) as { elasticsearchClient: any }).elasticsearchClient, { + ...args, + filter: input.and, + }); + }, }; } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index da8315c4a4ed7..17f0af4c9689e 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -10,7 +10,7 @@ import uniqBy from 'lodash.uniqby'; import { evaluate } from 'tinymath'; import { groupBy, zipObject, omit } from 'lodash'; import moment from 'moment'; -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Datatable, DatatableRow, @@ -39,7 +39,7 @@ function keysOf(obj: T): K[] { type Arguments = { [key in PointSeriesColumnName]: string | null }; -export function pointseries(): ExpressionFunction< +export function pointseries(): ExpressionFunctionDefinition< 'pointseries', Datatable, Arguments, @@ -50,10 +50,10 @@ export function pointseries(): ExpressionFunction< return { name: 'pointseries', type: 'pointseries', - help, context: { types: ['datatable'], }, + help, args: { color: { types: ['string'], @@ -78,11 +78,11 @@ export function pointseries(): ExpressionFunction< // In the future it may make sense to add things like shape, or tooltip values, but I think what we have is good for now // The way the function below is written you can add as many arbitrary named args as you want. }, - fn: (context, args) => { + fn: (input, args) => { const errors = getFunctionErrors().pointseries; // Note: can't replace pivotObjectArray with datatableToMathContext, lose name of non-numeric columns - const columnNames = context.columns.map(col => col.name); - const mathScope = pivotObjectArray(context.rows, columnNames); + const columnNames = input.columns.map(col => col.name); + const mathScope = pivotObjectArray(input.rows, columnNames); const autoQuoteColumn = (col: string | null) => { if (!col || !columnNames.includes(col)) { return col; @@ -117,7 +117,7 @@ export function pointseries(): ExpressionFunction< name: argName, value: mathExp, }); - col.type = getExpressionType(context.columns, mathExp); + col.type = getExpressionType(input.columns, mathExp); col.role = 'dimension'; } else { measureNames.push(argName); @@ -131,13 +131,13 @@ export function pointseries(): ExpressionFunction< }); const PRIMARY_KEY = '%%CANVAS_POINTSERIES_PRIMARY_KEY%%'; - const rows: DatatableRow[] = context.rows.map((row, i) => ({ + const rows: DatatableRow[] = input.rows.map((row, i) => ({ ...row, [PRIMARY_KEY]: i, })); function normalizeValue(expression: string, value: string) { - switch (getExpressionType(context.columns, expression)) { + switch (getExpressionType(input.columns, expression)) { case 'string': return String(value); case 'number': @@ -186,7 +186,7 @@ export function pointseries(): ExpressionFunction< // Then compute that 1 value for each measure Object.values(measureKeys).forEach(valueRows => { - const subtable = { type: 'datatable', columns: context.columns, rows: valueRows }; + const subtable = { type: 'datatable', columns: input.columns, rows: valueRows }; const subScope = pivotObjectArray( subtable.rows, subtable.columns.map(col => col.name) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx index 55f58efa37bf4..e60c99b683f34 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx @@ -7,11 +7,11 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { ExpressionAST } from '../../../../../types'; +import { ExpressionAstExpression } from '../../../../../types'; import { ExtendedTemplate } from '../extended_template'; -const defaultExpression: ExpressionAST = { +const defaultExpression: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -29,7 +29,7 @@ const defaultValues = { class Interactive extends React.Component<{}, typeof defaultValues> { public state = defaultValues; - _onValueChange: (argValue: ExpressionAST) => void = argValue => { + _onValueChange: (argValue: ExpressionAstExpression) => void = argValue => { action('onValueChange')(argValue); this.setState({ argValue }); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 806a61042494f..ec92e93368535 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -9,14 +9,14 @@ import PropTypes from 'prop-types'; import { EuiSelect, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; -import { ExpressionAST } from '../../../../types'; +import { ExpressionAstExpression } from '../../../../types'; import { ArgumentStrings } from '../../../../i18n/ui'; const { AxisConfig: strings } = ArgumentStrings; const { set } = immutable; -const defaultExpression: ExpressionAST = { +const defaultExpression: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -28,8 +28,8 @@ const defaultExpression: ExpressionAST = { }; export interface Props { - onValueChange: (newValue: ExpressionAST) => void; - argValue: boolean | ExpressionAST; + onValueChange: (newValue: ExpressionAstExpression) => void; + argValue: boolean | ExpressionAstExpression; typeInstance: { name: 'xaxis' | 'yaxis'; }; diff --git a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts index dbe81deced36d..31d213f4853ff 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts @@ -35,7 +35,7 @@ describe('autocomplete', () => { const expression = 'plot '; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const plotFn = functionSpecs.find(spec => spec.name === 'plot'); - expect(suggestions.length).toBe(Object.keys(plotFn.args).length); + expect(suggestions.length).toBe(Object.keys(plotFn!.args).length); expect(suggestions[0].start).toBe(expression.length); expect(suggestions[0].end).toBe(expression.length); }); @@ -44,7 +44,7 @@ describe('autocomplete', () => { const expression = 'shape shape='; const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - expect(suggestions.length).toBe(shapeFn.args.shape.options.length); + expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length); expect(suggestions[0].end).toBe(expression.length); }); @@ -82,27 +82,24 @@ describe('autocomplete', () => { expect(suggestions.length).toBe(functionSpecs.length); expect(suggestions[0].fnDef.type).toBe('datatable'); - expect(suggestions[0].fnDef.context && suggestions[0].fnDef.context.types).toEqual([ - 'datatable', - ]); + expect(suggestions[0].fnDef.inputTypes).toEqual(['datatable']); const withReturnOnly = suggestions.findIndex( suggestion => suggestion.fnDef.type === 'datatable' && - suggestion.fnDef.context && - suggestion.fnDef.context.types && - !(suggestion.fnDef.context.types as string[]).includes('datatable') + suggestion.fnDef.inputTypes && + !(suggestion.fnDef.inputTypes as string[]).includes('datatable') ); const withNeither = suggestions.findIndex( suggestion => suggestion.fnDef.type !== 'datatable' && - (!suggestion.fnDef.context || - !(suggestion.fnDef.context.types as string[]).includes('datatable')) + (!suggestion.fnDef.inputTypes || + !(suggestion.fnDef.inputTypes as string[]).includes('datatable')) ); expect(suggestions[0].fnDef.type).toBe('datatable'); - expect(suggestions[0].fnDef.context?.types).toEqual(['datatable']); + expect(suggestions[0].fnDef.inputTypes).toEqual(['datatable']); expect(withReturnOnly).toBeLessThan(withNeither); }); @@ -115,7 +112,7 @@ describe('autocomplete', () => { expression.length - 1 ); const ltFn = functionSpecs.find(spec => spec.name === 'lt'); - expect(suggestions.length).toBe(Object.keys(ltFn.args).length); + expect(suggestions.length).toBe(Object.keys(ltFn!.args).length); expect(suggestions[0].start).toBe(expression.length - 1); expect(suggestions[0].end).toBe(expression.length - 1); }); @@ -128,7 +125,7 @@ describe('autocomplete', () => { expression.length - 1 ); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - expect(suggestions.length).toBe(shapeFn.args.shape.options.length); + expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length - 1); expect(suggestions[0].end).toBe(expression.length - 1); }); @@ -141,7 +138,7 @@ describe('autocomplete', () => { expression.length - 1 ); const shapeFn = functionSpecs.find(spec => spec.name === 'shape'); - expect(suggestions.length).toBe(shapeFn.args.shape.options.length); + expect(suggestions.length).toBe(shapeFn!.args.shape.options.length); expect(suggestions[0].start).toBe(expression.length - '"ar"'.length); expect(suggestions[0].end).toBe(expression.length); }); diff --git a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts index 96917e3e7ed2c..50341c977d6d9 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts @@ -6,15 +6,15 @@ import { uniq } from 'lodash'; // @ts-ignore Untyped Library -import { parse, getByAlias as untypedGetByAlias } from '@kbn/interpreter/common'; +import { parse } from '@kbn/interpreter/common'; import { - ExpressionAST, - ExpressionFunctionAST, - ExpressionArgAST, - CanvasFunction, - CanvasArg, - CanvasArgValue, -} from '../../types'; + ExpressionAstExpression, + ExpressionAstFunction, + ExpressionAstArgument, + ExpressionFunction, + ExpressionFunctionParameter, + getByAlias, +} from '../../../../../../src/plugins/expressions'; const MARKER = 'CANVAS_SUGGESTION_MARKER'; @@ -26,12 +26,12 @@ interface BaseSuggestion { export interface FunctionSuggestion extends BaseSuggestion { type: 'function'; - fnDef: CanvasFunction; + fnDef: ExpressionFunction; } -type ArgSuggestionValue = CanvasArgValue & { +interface ArgSuggestionValue extends Omit { name: string; -}; +} interface ArgSuggestion extends BaseSuggestion { type: 'argument'; @@ -71,18 +71,18 @@ interface ASTMetaInformation { node: T; } -// Wraps ExpressionArg with meta or replace ExpressionAST with ExpressionASTWithMeta -type WrapExpressionArgWithMeta = T extends ExpressionAST +// Wraps ExpressionArg with meta or replace ExpressionAstExpression with ExpressionASTWithMeta +type WrapExpressionArgWithMeta = T extends ExpressionAstExpression ? ExpressionASTWithMeta : ASTMetaInformation; -type ExpressionArgASTWithMeta = WrapExpressionArgWithMeta; +type ExpressionArgASTWithMeta = WrapExpressionArgWithMeta; type Modify = Pick> & R; // Wrap ExpressionFunctionAST with meta and modify arguments to be wrapped with meta type ExpressionFunctionASTWithMeta = Modify< - ExpressionFunctionAST, + ExpressionAstFunction, { arguments: { [key: string]: ExpressionArgASTWithMeta[]; @@ -93,7 +93,7 @@ type ExpressionFunctionASTWithMeta = Modify< // Wrap ExpressionFunctionAST with meta and modify chain to be wrapped with meta type ExpressionASTWithMeta = ASTMetaInformation< Modify< - ExpressionAST, + ExpressionAstExpression, { chain: Array>; } @@ -107,23 +107,12 @@ function isExpression( return typeof maybeExpression.node === 'object'; } -// Overloads to change return type based on specs -function getByAlias(specs: CanvasFunction[], name: string): CanvasFunction; -// eslint-disable-next-line @typescript-eslint/unified-signatures -function getByAlias(specs: CanvasArg, name: string): CanvasArgValue; -function getByAlias( - specs: CanvasFunction[] | CanvasArg, - name: string -): CanvasFunction | CanvasArgValue { - return untypedGetByAlias(specs, name); -} - /** * Generates the AST with the given expression and then returns the function and argument definitions * at the given position in the expression, if there are any. */ export function getFnArgDefAtPosition( - specs: CanvasFunction[], + specs: ExpressionFunction[], expression: string, position: number ) { @@ -155,7 +144,7 @@ export function getFnArgDefAtPosition( * an unnamed argument, we suggest argument names. If it turns into a value, we suggest values. */ export function getAutocompleteSuggestions( - specs: CanvasFunction[], + specs: ExpressionFunction[], expression: string, position: number ): AutocompleteSuggestion[] { @@ -268,7 +257,7 @@ function getFnArgAtPosition(ast: ExpressionASTWithMeta, position: number): FnArg } function getFnNameSuggestions( - specs: CanvasFunction[], + specs: ExpressionFunction[], ast: ExpressionASTWithMeta, fnIndex: number ): FunctionSuggestion[] { @@ -284,11 +273,11 @@ function getFnNameSuggestions( const prevFnType = prevFnDef && prevFnDef.type; const nextFnDef = nextFn && getByAlias(specs, nextFn.node.function); - const nextFnContext = nextFnDef && nextFnDef.context && nextFnDef.context.types; + const nextFnInputTypes = nextFnDef && nextFnDef.inputTypes; - const fnDefs = specs.sort((a: CanvasFunction, b: CanvasFunction): number => { - const aScore = getScore(a, prevFnType, nextFnContext, false); - const bScore = getScore(b, prevFnType, nextFnContext, false); + const fnDefs = specs.sort((a: ExpressionFunction, b: ExpressionFunction): number => { + const aScore = getScore(a, prevFnType, nextFnInputTypes, false); + const bScore = getScore(b, prevFnType, nextFnInputTypes, false); if (aScore === bScore) { return a.name < b.name ? -1 : 1; @@ -302,7 +291,7 @@ function getFnNameSuggestions( } function getSubFnNameSuggestions( - specs: CanvasFunction[], + specs: ExpressionFunction[], ast: ExpressionASTWithMeta, fnIndex: number, parentFn: string, @@ -315,7 +304,7 @@ function getSubFnNameSuggestions( const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query)); const parentFnDef = getByAlias(specs, parentFn); - const matchingArgDef = getByAlias(parentFnDef.args, parentFnArgName); + const matchingArgDef = getByAlias(parentFnDef!.args, parentFnArgName); if (!matchingArgDef) { return []; @@ -326,7 +315,7 @@ function getSubFnNameSuggestions( const expectedReturnTypes = matchingArgDef.types; - const fnDefs = matchingFnDefs.sort((a: CanvasFunction, b: CanvasFunction) => { + const fnDefs = matchingFnDefs.sort((a: ExpressionFunction, b: ExpressionFunction) => { const aScore = getScore(a, contextFnType, expectedReturnTypes, true); const bScore = getScore(b, contextFnType, expectedReturnTypes, true); @@ -342,7 +331,7 @@ function getSubFnNameSuggestions( } function getScore( - func: CanvasFunction, + func: ExpressionFunction, contextType: any, returnTypes?: any[] | null, isSubFunc?: boolean @@ -352,10 +341,7 @@ function getScore( contextType = 'null'; } - let funcContextTypes = []; - if (func.context && func.context.types && func.context.types.length) { - funcContextTypes = func.context.types; - } + const inputTypesNormalized = (func.inputTypes || []) as string[]; if (isSubFunc) { if (returnTypes && func.type) { @@ -364,21 +350,21 @@ function getScore( if (returnTypes.length && returnTypes.includes(func.type)) { score++; - if (funcContextTypes.includes(contextType)) { + if (inputTypesNormalized.includes(contextType)) { score++; } } } } else { - if (func.context && func.context.types) { - const expectsNull = (funcContextTypes as string[]).includes('null'); + if (func.inputTypes) { + const expectsNull = inputTypesNormalized.includes('null'); if (!expectsNull && contextType !== 'null') { // If not in a sub-expression and there's a preceding function, // favor functions that expect a context with top results matching the passed in context score++; - if (func.context.types.includes(contextType)) { + if (func.inputTypes.includes(contextType)) { score++; } } else if (expectsNull && contextType === 'null') { @@ -397,7 +383,7 @@ function getScore( } function getArgNameSuggestions( - specs: CanvasFunction[], + specs: ExpressionFunction[], ast: ExpressionASTWithMeta, fnIndex: number, argName: string, @@ -420,29 +406,35 @@ function getArgNameSuggestions( } ); - const unusedArgDefs = Object.entries(fnDef.args).filter( - ([matchingArgName, matchingArgDef]) => { - if (matchingArgDef.multi) { - return true; - } - return !argEntries.some(([name, values]) => { - return ( - values.length > 0 && - (name === matchingArgName || (matchingArgDef.aliases || []).includes(name)) - ); - }); + const unusedArgDefs = Object.entries(fnDef.args).filter(([matchingArgName, matchingArgDef]) => { + if (matchingArgDef.multi) { + return true; } - ); + return !argEntries.some(([name, values]) => { + return ( + values.length > 0 && + (name === matchingArgName || (matchingArgDef.aliases || []).includes(name)) + ); + }); + }); - const argDefs = unusedArgDefs.map(([name, arg]) => ({ name, ...arg })).sort(unnamedArgComparator); + const argDefs: ArgSuggestionValue[] = unusedArgDefs + .map(([name, arg]) => ({ name, ...arg })) + .sort(unnamedArgComparator); return argDefs.map(argDef => { - return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef }; + return { + type: 'argument', + text: argDef.name + '=', + start, + end: end - MARKER.length, + argDef, + }; }); } function getArgValueSuggestions( - specs: CanvasFunction[], + specs: ExpressionFunction[], ast: ExpressionASTWithMeta, fnIndex: number, argName: string, @@ -492,7 +484,7 @@ function maybeQuote(value: any) { return value; } -function unnamedArgComparator(a: CanvasArgValue, b: CanvasArgValue): number { +function unnamedArgComparator(a: { aliases?: string[] }, b: { aliases?: string[] }): number { return ( (b.aliases && b.aliases.includes('_') ? 1 : 0) - (a.aliases && a.aliases.includes('_') ? 1 : 0) ); diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts index 94d7e6f43326f..565cfa251e126 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; -import { CanvasFunction } from '../../types'; -import { UnionToIntersection } from '../../types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; +import { UnionToIntersection, CanvasFunction } from '../../types'; import { help as all } from './dict/all'; import { help as alterColumn } from './dict/alter_column'; @@ -109,11 +108,11 @@ import { help as urlparam } from './dict/urlparam'; * This allows one to ensure each argument is present, and no extraneous arguments * remain. */ -export type FunctionHelp = T extends ExpressionFunction< +export type FunctionHelp = T extends ExpressionFunctionDefinition< infer Name, - infer Context, + infer Input, infer Arguments, - infer Return + infer Output > ? { help: string; @@ -137,11 +136,11 @@ export type FunctionHelp = T extends ExpressionFunction< // // Given a collection of functions, the map would contain each entry. // -type FunctionHelpMap = T extends ExpressionFunction< +type FunctionHelpMap = T extends ExpressionFunctionDefinition< infer Name, - infer Context, + infer Input, infer Arguments, - infer Return + infer Output > ? { [key in Name]: FunctionHelp } : never; @@ -155,8 +154,8 @@ type FunctionHelpDict = UnionToIntersection>; /** * Help text for Canvas Functions should be properly localized. This function will - * return a dictionary of help strings, organized by `CanvasFunction` specification - * and then by available arguments within each `CanvasFunction`. + * return a dictionary of help strings, organized by `ExpressionFunctionDefinition` + * specification and then by available arguments within each `ExpressionFunctionDefinition`. * * This a function, rather than an object, to future-proof string initialization, * if ever necessary. diff --git a/x-pack/legacy/plugins/canvas/public/browser_functions.js b/x-pack/legacy/plugins/canvas/public/browser_functions.ts similarity index 67% rename from x-pack/legacy/plugins/canvas/public/browser_functions.js rename to x-pack/legacy/plugins/canvas/public/browser_functions.ts index 5be270362b63f..011fe8b4504bc 100644 --- a/x-pack/legacy/plugins/canvas/public/browser_functions.js +++ b/x-pack/legacy/plugins/canvas/public/browser_functions.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { functionsRegistry } from 'plugins/interpreter/registries'; +import { npSetup } from 'ui/new_platform'; import { functions } from '../canvas_plugin_src/functions/browser'; -functions.forEach(fn => { - functionsRegistry.register(fn); -}); +functions.forEach(npSetup.plugins.expressions.registerFunction); +// eslint-disable-next-line import/no-default-export export default functions; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_content/index.js b/x-pack/legacy/plugins/canvas/public/components/element_content/index.js index bf7b0ce40fc0e..f05222452b1ee 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_content/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_content/index.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { get } from 'lodash'; -import { registries } from 'plugins/interpreter/registries'; +import { npStart } from 'ui/new_platform'; import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; import { ElementContent as Component } from './element_content'; @@ -19,7 +19,7 @@ const mapStateToProps = state => ({ export const ElementContent = compose( connect(mapStateToProps), withProps(({ renderable }) => ({ - renderFunction: registries.renderers.get(get(renderable, 'as')), + renderFunction: npStart.plugins.expressions.getRenderer(get(renderable, 'as')), })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/expression_input/expression_input.tsx b/x-pack/legacy/plugins/canvas/public/components/expression_input/expression_input.tsx index c33e91064dc84..9653decb6db97 100644 --- a/x-pack/legacy/plugins/canvas/public/components/expression_input/expression_input.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/expression_input/expression_input.tsx @@ -9,10 +9,8 @@ import PropTypes from 'prop-types'; import { EuiFormRow } from '@elastic/eui'; import { debounce } from 'lodash'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; - +import { ExpressionFunction } from '../../../../../../../src/plugins/expressions'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; - -import { CanvasFunction } from '../../../types'; import { AutocompleteSuggestion, getAutocompleteSuggestions, @@ -27,7 +25,7 @@ interface Props { /** Font size of text within the editor */ /** Canvas function defintions */ - functionDefinitions: CanvasFunction[]; + functionDefinitions: ExpressionFunction[]; /** Optional string for displaying error messages */ error?: string; diff --git a/x-pack/legacy/plugins/canvas/public/components/expression_input/reference.ts b/x-pack/legacy/plugins/canvas/public/components/expression_input/reference.ts index 3a5030c492b25..ca3819195fcbd 100644 --- a/x-pack/legacy/plugins/canvas/public/components/expression_input/reference.ts +++ b/x-pack/legacy/plugins/canvas/public/components/expression_input/reference.ts @@ -3,21 +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 { CanvasFunction, CanvasArgValue } from '../../../types'; import { ComponentStrings } from '../../../i18n'; +import { + ExpressionFunction, + ExpressionFunctionParameter, +} from '../../../../../../../src/plugins/expressions'; const { ExpressionInput: strings } = ComponentStrings; /** - * Given a function definition, this function returns a markdown string + * Given an expression function, this function returns a markdown string * that includes the context the function accepts, what the function returns * as well as the general help/documentation text associated with the function */ -export function getFunctionReferenceStr(fnDef: CanvasFunction) { - const { help, context, type } = fnDef; - - const acceptTypes = context && context.types ? context.types.join(' | ') : 'null'; +export function getFunctionReferenceStr(fnDef: ExpressionFunction) { + const { help, type, inputTypes } = fnDef; + const acceptTypes = inputTypes ? inputTypes.join(' | ') : 'null'; const returnType = type ? type : 'null'; const doc = `${strings.getFunctionReferenceAcceptsDetail( @@ -29,12 +31,12 @@ export function getFunctionReferenceStr(fnDef: CanvasFunction) { } /** - * Given an argument defintion, this function returns a markdown string + * Given an argument definition, this function returns a markdown string * that includes the aliases of the argument, types accepted for the argument, * the default value of the argument, whether or not its required, and * the general help/documentation text associated with the argument */ -export function getArgReferenceStr(argDef: CanvasArgValue) { +export function getArgReferenceStr(argDef: Omit) { const { aliases, types, default: def, required, help } = argDef; const secondLineArr = []; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx index 58af29463c3eb..7e00bd4f33a8a 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx @@ -10,9 +10,9 @@ import { withKnobs, array, radios, boolean } from '@storybook/addon-knobs'; import React from 'react'; import { ExtendedTemplate } from '../extended_template'; -import { ExpressionAST } from '../../../../../types'; +import { ExpressionAstExpression } from '../../../../../types'; -const defaultExpression: ExpressionAST = { +const defaultExpression: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -27,7 +27,7 @@ const defaultValues = { argValue: defaultExpression, }; -class Interactive extends React.Component<{}, { argValue: ExpressionAST }> { +class Interactive extends React.Component<{}, { argValue: ExpressionAstExpression }> { public state = defaultValues; public render() { diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx index 7a35f4de79809..037b15d5c51e9 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx @@ -11,9 +11,9 @@ import React from 'react'; import { getDefaultWorkpad } from '../../../../state/defaults'; import { SimpleTemplate } from '../simple_template'; -import { ExpressionAST } from '../../../../../types'; +import { ExpressionAstExpression } from '../../../../../types'; -const defaultExpression: ExpressionAST = { +const defaultExpression: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -28,7 +28,7 @@ const defaultValues = { argValue: defaultExpression, }; -class Interactive extends React.Component<{}, { argValue: ExpressionAST }> { +class Interactive extends React.Component<{}, { argValue: ExpressionAstExpression }> { public state = defaultValues; public render() { diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx index 3c0b034da0360..615179a3f6f68 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer } from '@elastic/eui'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; -import { ExpressionAST } from '../../../../types'; +import { ExpressionAstExpression } from '../../../../types'; import { ArgTypesStrings } from '../../../../i18n'; const { set, del } = immutable; @@ -24,9 +24,9 @@ export interface Arguments { export type Argument = keyof Arguments; export interface Props { - argValue: ExpressionAST; + argValue: ExpressionAstExpression; labels: string[]; - onValueChange: (argValue: ExpressionAST) => void; + onValueChange: (argValue: ExpressionAstExpression) => void; typeInstance?: { name: string; options: { diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts index c3211c27eef75..3e8ef4d89991a 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts @@ -10,13 +10,13 @@ import { get } from 'lodash'; import { templateFromReactComponent } from '../../../lib/template_from_react_component'; import { SimpleTemplate } from './simple_template'; import { ExtendedTemplate, Props as ExtendedTemplateProps } from './extended_template'; -import { ExpressionAST } from '../../../../types'; +import { ExpressionAstExpression } from '../../../../types'; import { ArgTypesStrings } from '../../../../i18n'; const { SeriesStyle: strings } = ArgTypesStrings; interface Props { - argValue: ExpressionAST; + argValue: ExpressionAstExpression; renderError: Function; setLabel: Function; label: string; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx index ba1f4305167a4..226122cf0b25f 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx @@ -11,7 +11,7 @@ import immutable from 'object-path-immutable'; import { get } from 'lodash'; import { ColorPickerPopover } from '../../../components/color_picker_popover'; import { TooltipIcon, IconType } from '../../../components/tooltip_icon'; -import { ExpressionAST, CanvasWorkpad } from '../../../../types'; +import { ExpressionAstExpression, CanvasWorkpad } from '../../../../types'; import { ArgTypesStrings } from '../../../../i18n'; const { set, del } = immutable; @@ -23,9 +23,9 @@ interface Arguments { type Argument = keyof Arguments; interface Props { - argValue: ExpressionAST; + argValue: ExpressionAstExpression; labels?: string[]; - onValueChange: (argValue: ExpressionAST) => void; + onValueChange: (argValue: ExpressionAstExpression) => void; typeInstance: { name: string; }; diff --git a/x-pack/legacy/plugins/canvas/public/functions/asset.ts b/x-pack/legacy/plugins/canvas/public/functions/asset.ts index 7f2f56a71756e..2f2ad181b264c 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/asset.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/asset.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; // @ts-ignore unconverted local lib import { getState } from '../state/store'; import { getAssetById } from '../state/selectors/assets'; @@ -14,7 +14,7 @@ interface Arguments { id: string; } -export function asset(): ExpressionFunction<'asset', null, Arguments, string> { +export function asset(): ExpressionFunctionDefinition<'asset', null, Arguments, string> { const { help, args: argHelp } = getFunctionHelp().asset; const errors = getFunctionErrors().asset; @@ -22,10 +22,8 @@ export function asset(): ExpressionFunction<'asset', null, Arguments, string> { name: 'asset', aliases: [], type: 'string', + inputTypes: ['null'], help, - context: { - types: ['null'], - }, args: { id: { aliases: ['_'], @@ -34,7 +32,7 @@ export function asset(): ExpressionFunction<'asset', null, Arguments, string> { required: true, }, }, - fn: (_context, args) => { + fn: (input, args) => { const assetId = args.id; const storedAsset = getAssetById(getState(), assetId); if (storedAsset !== undefined) { diff --git a/x-pack/legacy/plugins/canvas/public/functions/filters.ts b/x-pack/legacy/plugins/canvas/public/functions/filters.ts index 722cf5a9d5eba..44b321e00091a 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/filters.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/filters.ts @@ -10,7 +10,7 @@ import { get } from 'lodash'; import { interpretAst } from 'plugins/interpreter/interpreter'; // @ts-ignore untyped Elastic lib import { registries } from 'plugins/interpreter/registries'; -import { ExpressionFunction } from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; // @ts-ignore untyped local import { getState } from '../state/store'; import { getGlobalFilters } from '../state/selectors/workpad'; @@ -43,16 +43,14 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = }); } -export function filters(): ExpressionFunction<'filters', null, Arguments, Filter> { +export function filters(): ExpressionFunctionDefinition<'filters', null, Arguments, Filter> { const { help, args: argHelp } = getFunctionHelp().filters; return { name: 'filters', type: 'filter', help, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { group: { aliases: ['_'], @@ -67,7 +65,7 @@ export function filters(): ExpressionFunction<'filters', null, Arguments, Filter default: false, }, }, - fn: (_context, { group, ungrouped }) => { + fn: (input, { group, ungrouped }) => { const filterList = getFiltersByGroup(getGlobalFilters(getState()), group, ungrouped); if (filterList && filterList.length) { diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index 4377f2cb4d53b..ae87e858cf796 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -9,7 +9,7 @@ import moment from 'moment-timezone'; import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { TimeRange } from 'src/plugins/data/common'; -import { ExpressionFunction, DatatableRow } from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; // @ts-ignore untyped local import { buildBoolArray } from '../../server/lib/build_bool_array'; @@ -44,16 +44,19 @@ function parseDateMath(timeRange: TimeRange, timeZone: string) { return parsedRange; } -export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Promise> { +export function timelion(): ExpressionFunctionDefinition< + 'timelion', + Filter, + Arguments, + Promise +> { const { help, args: argHelp } = getFunctionHelp().timelion; return { name: 'timelion', type: 'datatable', + inputTypes: ['filter'], help, - context: { - types: ['filter'], - }, args: { query: { types: ['string'], @@ -82,10 +85,10 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr default: 'UTC', }, }, - fn: (context, args): Promise => { + fn: (input, args): Promise => { // Timelion requires a time range. Use the time range from the timefilter element in the // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = context.and.find(and => and.type === 'time'); + const timeFilter = input.and.find(and => and.type === 'time'); const range = timeFilter ? { min: timeFilter.from, max: timeFilter.to } : parseDateMath({ from: args.from, to: args.to }, args.timezone); @@ -95,7 +98,7 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr es: { filter: { bool: { - must: buildBoolArray(context.and), + must: buildBoolArray(input.and), }, }, }, diff --git a/x-pack/legacy/plugins/canvas/public/functions/to.ts b/x-pack/legacy/plugins/canvas/public/functions/to.ts index 35d4ea21097ee..7c24926b5aa6a 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/to.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/to.ts @@ -6,16 +6,15 @@ // @ts-ignore untyped Elastic library import { castProvider } from '@kbn/interpreter/common'; -import { ExpressionFunction } from 'src/plugins/expressions/public'; -// @ts-ignore untyped Elastic library -import { registries } from 'plugins/interpreter/registries'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; +import { npStart } from 'ui/new_platform'; import { getFunctionHelp, getFunctionErrors } from '../../i18n'; interface Arguments { type: string[]; } -export function to(): ExpressionFunction<'to', any, Arguments, any> { +export function to(): ExpressionFunctionDefinition<'to', any, Arguments, any> { const { help, args: argHelp } = getFunctionHelp().to; const errors = getFunctionErrors().to; @@ -31,12 +30,12 @@ export function to(): ExpressionFunction<'to', any, Arguments, any> { multi: true, }, }, - fn: (context, args) => { + fn: (input, args) => { if (!args.type) { throw errors.missingType(); } - return castProvider(registries.types.toJS())(context, args.type); + return castProvider(npStart.plugins.expressions.getTypes())(input, args.type); }, }; } diff --git a/x-pack/legacy/plugins/canvas/public/lib/function_definitions.js b/x-pack/legacy/plugins/canvas/public/lib/function_definitions.js index 71d6aac7ad901..36ad0ba0b0015 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/function_definitions.js +++ b/x-pack/legacy/plugins/canvas/public/lib/function_definitions.js @@ -5,10 +5,13 @@ */ import uniqBy from 'lodash.uniqby'; -import { registries } from 'plugins/interpreter/registries'; +import { npStart } from 'ui/new_platform'; import { getServerFunctions } from '../state/selectors/app'; export async function getFunctionDefinitions(state) { const serverFunctions = getServerFunctions(state); - return uniqBy(serverFunctions.concat(registries.browserFunctions.toArray()), 'name'); + return uniqBy( + serverFunctions.concat(Object.values(npStart.plugins.expressions.getFunctions())), + 'name' + ); } diff --git a/x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts b/x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts index 7e51cb8057658..e15be9a90beb0 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/monaco_language_def.ts @@ -5,11 +5,7 @@ */ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; - -// @ts-ignore -import { registries } from 'plugins/interpreter/registries'; - -import { CanvasFunction } from '../../types'; +import { npSetup } from 'ui/new_platform'; export const LANGUAGE_ID = 'canvas-expression'; @@ -99,8 +95,8 @@ export const language: Language = { }; export function registerLanguage() { - const functions = registries.browserFunctions.toArray(); - language.keywords = functions.map((fn: CanvasFunction) => fn.name); + const functions = Object.values(npSetup.plugins.expressions.getFunctions()); + language.keywords = functions.map(({ name }) => name); monaco.languages.register({ id: LANGUAGE_ID }); monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language); diff --git a/x-pack/legacy/plugins/canvas/public/renderers.js b/x-pack/legacy/plugins/canvas/public/renderers.js index 717daae7fa9d0..0c278789bc1aa 100644 --- a/x-pack/legacy/plugins/canvas/public/renderers.js +++ b/x-pack/legacy/plugins/canvas/public/renderers.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderersRegistry } from 'plugins/interpreter/registries'; +import { npSetup } from 'ui/new_platform'; import { renderFunctions } from '../canvas_plugin_src/renderers'; -renderFunctions.forEach(r => { - renderersRegistry.register(r); -}); +renderFunctions.forEach(npSetup.plugins.expressions.registerRenderer); export default renderFunctions; diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index d47a339cf8afe..84fab0cb0ae6d 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -11,14 +11,12 @@ import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/comm import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types'; +import { ExpressionContext, CanvasGroup, PositionedElement } from '../../../types'; import { - ExpressionAST, - ExpressionFunctionAST, - ExpressionArgAST, - ExpressionContext, - CanvasGroup, - PositionedElement, -} from '../../../types'; + ExpressionAstArgument, + ExpressionAstFunction, + ExpressionAstExpression, +} from '../../../../../../../src/plugins/expressions/common'; type Modify = Pick> & R; type WorkpadInfo = Modify; @@ -27,7 +25,7 @@ const workpadRoot = 'persistent.workpad'; const appendAst = (element: CanvasElement): PositionedElement => ({ ...element, - ast: safeElementFromExpression(element.expression) as ExpressionAST, + ast: safeElementFromExpression(element.expression) as ExpressionAstExpression, }); // workpad getters @@ -188,33 +186,35 @@ export function getGlobalFilters(state: State): string[] { } type onValueFunction = ( - argValue: ExpressionArgAST, + argValue: ExpressionAstArgument, argNames?: string, - args?: ExpressionFunctionAST['arguments'] -) => ExpressionArgAST | ExpressionArgAST[] | undefined; + args?: ExpressionAstFunction['arguments'] +) => ExpressionAstArgument | ExpressionAstArgument[] | undefined; -function buildGroupValues(args: ExpressionFunctionAST['arguments'], onValue: onValueFunction) { +function buildGroupValues(args: ExpressionAstFunction['arguments'], onValue: onValueFunction) { const argNames = Object.keys(args); - return argNames.reduce((values, argName) => { + return argNames.reduce((values, argName) => { // we only care about group values if (argName !== '_' && argName !== 'group') { return values; } - return args[argName].reduce((acc, argValue) => { + return args[argName].reduce((acc, argValue) => { // delegate to passed function to buyld list return acc.concat(onValue(argValue, argName, args) || []); }, values); }, []); } -function extractFilterGroups(ast: ExpressionAST): ExpressionArgAST[] { +function extractFilterGroups( + ast: ExpressionAstExpression | ExpressionAstFunction +): ExpressionAstArgument[] { if (ast.type !== 'expression') { throw new Error('AST must be an expression'); } - return ast.chain.reduce((groups, item) => { + return ast.chain.reduce((groups, item) => { // TODO: we always get a function here, right? const { function: fn, arguments: args } = item; @@ -247,8 +247,11 @@ export function getGlobalFilterGroups(state: State) { // check that a filter is defined if (el.filter != null && el.filter.length) { // extract the filter group - const filterAst = fromExpression(el.filter) as ExpressionAST; - const filterGroup: ExpressionArgAST = get(filterAst, `chain[0].arguments.filterGroup[0]`); + const filterAst = fromExpression(el.filter) as ExpressionAstExpression; + const filterGroup: ExpressionAstArgument = get( + filterAst, + `chain[0].arguments.filterGroup[0]` + ); // add any new group to the array if (filterGroup && filterGroup !== '' && !acc.includes(String(filterGroup))) { @@ -258,7 +261,9 @@ export function getGlobalFilterGroups(state: State) { // extract groups from all expressions that use filters function if (el.expression != null && el.expression.length) { - const expressionAst = fromExpression(el.expression) as ExpressionAST; + const expressionAst = fromExpression(el.expression) as + | ExpressionAstFunction + | ExpressionAstExpression; const groups = extractFilterGroups(expressionAst); groups.forEach(group => { if (!acc.includes(String(group))) { diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index 1f17e85bfd294..713747551ff47 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -12,12 +12,6 @@ export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { plugins.interpreter.register({ serverFunctions: functions }); - core.injectUiAppVars('canvas', async () => { - return { - ...plugins.kibana.injectedUiAppVars, - }; - }); - plugins.features.registerFeature({ id: 'canvas', name: 'Canvas', diff --git a/x-pack/legacy/plugins/canvas/server/shim.ts b/x-pack/legacy/plugins/canvas/server/shim.ts index 1ca6e28bd347e..c9d70e6a721ee 100644 --- a/x-pack/legacy/plugins/canvas/server/shim.ts +++ b/x-pack/legacy/plugins/canvas/server/shim.ts @@ -18,7 +18,6 @@ export interface CoreSetup { http: { route: Legacy.Server['route']; }; - injectUiAppVars: Legacy.Server['injectUiAppVars']; } export interface PluginsSetup { @@ -52,7 +51,6 @@ export async function createSetupShim( ...server.newPlatform.setup.core.http, route: (...args) => server.route(...args), }, - injectUiAppVars: server.injectUiAppVars, }, pluginsSetup: { // @ts-ignore: New Platform not typed diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts b/x-pack/legacy/plugins/canvas/tasks/mocks/uiAbsoluteToParsedUrl.ts similarity index 71% rename from x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts rename to x-pack/legacy/plugins/canvas/tasks/mocks/uiAbsoluteToParsedUrl.ts index 46178a7d02977..994bd99981fa2 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/uiAbsoluteToParsedUrl.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/timefilter', () => { - return {}; -}); +export function absoluteToParsedUrl(absoluteUrl: string, basePath = '') { + return 'parsed-url'; +} diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index 0ceeb7ba60ebc..acb1cb9cd7625 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionAST } from 'src/plugins/expressions/common'; +import { ExpressionAstExpression } from 'src/plugins/expressions'; import { CanvasElement } from '.'; export interface ElementSpec { @@ -79,4 +79,4 @@ export interface ElementPosition { parent: string | null; } -export type PositionedElement = CanvasElement & { ast: ExpressionAST }; +export type PositionedElement = CanvasElement & { ast: ExpressionAstExpression }; diff --git a/x-pack/legacy/plugins/canvas/types/functions.ts b/x-pack/legacy/plugins/canvas/types/functions.ts index 773c9c3020a85..27b2a04ebd6e3 100644 --- a/x-pack/legacy/plugins/canvas/types/functions.ts +++ b/x-pack/legacy/plugins/canvas/types/functions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunction } from 'src/plugins/expressions/common'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { functions as commonFunctions } from '../canvas_plugin_src/functions/common'; import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser'; import { functions as serverFunctions } from '../canvas_plugin_src/functions/server'; @@ -26,9 +26,6 @@ export type UnionToIntersection = */ export type ValuesOf = T[number]; -type valueof = T[keyof T]; -type ValuesOfUnion = T extends any ? valueof : never; - /** * A `ExpressionFunctionFactory` is a powerful type used for any function that produces * an `ExpressionFunction`. If it does not meet the signature for such a function, @@ -88,8 +85,8 @@ type ValuesOfUnion = T extends any ? valueof : never; * in Kibana and Canvas. */ // prettier-ignore -export type ExpressionFunctionFactory = -() => ExpressionFunction; +export type ExpressionFunctionFactory = + () => ExpressionFunctionDefinition; /** * `FunctionFactory` exists as a name shim between the `ExpressionFunction` type and @@ -99,8 +96,8 @@ export type ExpressionFunctionFactory = - FnFactory extends ExpressionFunctionFactory ? - ExpressionFunction : + FnFactory extends ExpressionFunctionFactory ? + ExpressionFunctionDefinition : never; type CommonFunction = FunctionFactory; @@ -111,19 +108,8 @@ type ClientFunctions = FunctionFactory; /** * A collection of all Canvas Functions. */ -export type CanvasFunction = CommonFunction | BrowserFunction | ServerFunction | ClientFunctions; - -/** - * A union type of all Canvas Function names. - */ -export type CanvasFunctionName = CanvasFunction['name']; -/** - * A union type of all Canvas Function argument objects. - */ -export type CanvasArg = CanvasFunction['args']; - -export type CanvasArgValue = ValuesOfUnion; +export type CanvasFunction = CommonFunction | BrowserFunction | ServerFunction | ClientFunctions; /** * Represents a function called by the `case` Function. diff --git a/x-pack/legacy/plugins/canvas/types/state.ts b/x-pack/legacy/plugins/canvas/types/state.ts index 171c5515fbb2a..13c8f7a9176ab 100644 --- a/x-pack/legacy/plugins/canvas/types/state.ts +++ b/x-pack/legacy/plugins/canvas/types/state.ts @@ -8,14 +8,14 @@ import { Datatable, Filter, ExpressionImage, + ExpressionFunction, KibanaContext, KibanaDatatable, PointSeries, Render, Style, Range, -} from 'src/plugins/expressions/common'; -import { CanvasFunction } from './functions'; +} from 'src/plugins/expressions'; import { AssetType } from './assets'; import { CanvasWorkpad } from './canvas'; @@ -33,8 +33,7 @@ export interface AppState { interface StoreAppState { basePath: string; - // TODO: These server functions are actually missing the fn because they are serialized from the server - serverFunctions: CanvasFunction[]; + serverFunctions: ExpressionFunction[]; ready: boolean; } diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index 5e4b21d9b56d6..440f7bdc42bcb 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -8,13 +8,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable } from '@elastic/eui'; -import { - ExpressionFunction, - KibanaDatatable, -} from '../../../../../../src/plugins/expressions/common'; import { LensMultiTable } from '../types'; import { - IInterpreterRenderFunction, + ExpressionFunctionDefinition, + ExpressionRenderDefinition, IInterpreterRenderHandlers, } from '../../../../../../src/plugins/expressions/public'; import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; @@ -25,7 +22,8 @@ export interface DatatableColumns { } interface Args { - columns: DatatableColumns; + title: string; + columns: DatatableColumns & { type: 'lens_datatable_columns' }; } export interface DatatableProps { @@ -39,14 +37,15 @@ export interface DatatableRender { value: DatatableProps; } -export const datatable: ExpressionFunction< +export const datatable: ExpressionFunctionDefinition< 'lens_datatable', - KibanaDatatable, + LensMultiTable, Args, DatatableRender -> = ({ +> = { name: 'lens_datatable', type: 'render', + inputTypes: ['lens_multitable'], help: i18n.translate('xpack.lens.datatable.expressionHelpLabel', { defaultMessage: 'Datatable renderer', }), @@ -62,10 +61,7 @@ export const datatable: ExpressionFunction< help: '', }, }, - context: { - types: ['lens_multitable'], - }, - fn(data: KibanaDatatable, args: Args) { + fn(data, args) { return { type: 'render', as: 'lens_datatable_renderer', @@ -75,12 +71,11 @@ export const datatable: ExpressionFunction< }, }; }, - // TODO the typings currently don't support custom type args. As soon as they do, this can be removed -} as unknown) as ExpressionFunction<'lens_datatable', KibanaDatatable, Args, DatatableRender>; +}; type DatatableColumnsResult = DatatableColumns & { type: 'lens_datatable_columns' }; -export const datatableColumns: ExpressionFunction< +export const datatableColumns: ExpressionFunctionDefinition< 'lens_datatable_columns', null, DatatableColumns, @@ -90,9 +85,7 @@ export const datatableColumns: ExpressionFunction< aliases: [], type: 'lens_datatable_columns', help: '', - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { columnIds: { types: ['string'], @@ -100,7 +93,7 @@ export const datatableColumns: ExpressionFunction< help: '', }, }, - fn: function fn(_context: unknown, args: DatatableColumns) { + fn: function fn(input: unknown, args: DatatableColumns) { return { type: 'lens_datatable_columns', ...args, @@ -110,13 +103,13 @@ export const datatableColumns: ExpressionFunction< export const getDatatableRenderer = ( formatFactory: FormatFactory -): IInterpreterRenderFunction => ({ +): ExpressionRenderDefinition => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { defaultMessage: 'Datatable', }), help: '', - validate: () => {}, + validate: () => undefined, reuseDomNode: true, render: async ( domNode: Element, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index c9b9a43376651..0e256d0ab181b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -18,7 +18,7 @@ import { createExpressionRendererMock, DatasourceMock, } from '../mocks'; -import { ExpressionRenderer } from 'src/plugins/expressions/public'; +import { ReactExpressionRendererType } from 'src/plugins/expressions/public'; import { DragDrop } from '../../drag_drop'; import { FrameLayout } from './frame_layout'; @@ -66,7 +66,7 @@ describe('editor_frame', () => { let mockVisualization2: jest.Mocked; let mockDatasource2: DatasourceMock; - let expressionRendererMock: ExpressionRenderer; + let expressionRendererMock: ReactExpressionRendererType; beforeEach(() => { mockVisualization = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 3284f69b503c5..399eaf5888286 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useReducer } from 'react'; import { CoreSetup, CoreStart } from 'src/core/public'; -import { ExpressionRenderer } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { Datasource, DatasourcePublicAPI, @@ -33,7 +33,7 @@ export interface EditorFrameProps { visualizationMap: Record; initialDatasourceId: string | null; initialVisualizationId: string | null; - ExpressionRenderer: ExpressionRenderer; + ExpressionRenderer: ReactExpressionRendererType; onError: (e: { message: string }) => void; core: CoreSetup | CoreStart; dateRange: { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index c020ce8b3c8d1..9729d6259f84a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -15,7 +15,7 @@ import { createMockFramePublicAPI, } from '../mocks'; import { act } from 'react-dom/test-utils'; -import { ExpressionRenderer } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; @@ -29,7 +29,7 @@ describe('suggestion_panel', () => { let mockVisualization: Visualization; let mockDatasource: DatasourceMock; - let expressionRendererMock: ExpressionRenderer; + let expressionRendererMock: ReactExpressionRendererType; let dispatchMock: jest.Mock; const suggestion1State = { suggestion1: true }; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 46e226afe9c59..1115126792c86 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; -import { ExpressionRenderer } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; @@ -49,7 +49,7 @@ export interface SuggestionPanelProps { visualizationMap: Record; visualizationState: unknown; dispatch: (action: Action) => void; - ExpressionRenderer: ExpressionRenderer; + ExpressionRenderer: ReactExpressionRendererType; frame: FramePublicAPI; stagedPreview?: PreviewState; } @@ -61,7 +61,7 @@ const PreviewRenderer = ({ }: { withLabel: boolean; expression: string; - ExpressionRendererComponent: ExpressionRenderer; + ExpressionRendererComponent: ReactExpressionRendererType; }) => { return (
{ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 74dacd50d7a15..929b4667aeb66 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; import { FramePublicAPI, TableSuggestion, Visualization } from '../../types'; import { createMockVisualization, @@ -29,7 +29,7 @@ describe('workspace_panel', () => { let mockVisualization2: jest.Mocked; let mockDatasource: DatasourceMock; - let expressionRendererMock: jest.Mock; + let expressionRendererMock: jest.Mock; let instance: ReactWrapper; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 1058ccd81d669..c2a5c16e405a2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -17,7 +17,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { CoreStart, CoreSetup } from 'src/core/public'; -import { ExpressionRenderer } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; import { DragDrop, DragContext } from '../../drag_drop'; @@ -41,7 +41,7 @@ export interface WorkspacePanelProps { >; framePublicAPI: FramePublicAPI; dispatch: (action: Action) => void; - ExpressionRenderer: ExpressionRenderer; + ExpressionRenderer: ReactExpressionRendererType; core: CoreStart | CoreSetup; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx index 1f0620c43f7f7..59e1378e63661 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx @@ -5,7 +5,7 @@ */ import { Embeddable } from './embeddable'; -import { ExpressionRendererProps } from 'src/plugins/expressions/public'; +import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, esFilters } from 'src/plugins/data/public'; import { Document } from '../../persistence'; @@ -31,7 +31,7 @@ const savedVis: Document = { describe('embeddable', () => { let mountpoint: HTMLDivElement; - let expressionRenderer: jest.Mock; + let expressionRenderer: jest.Mock; beforeEach(() => { mountpoint = document.createElement('div'); @@ -104,7 +104,6 @@ describe('embeddable', () => { embeddable.render(mountpoint); expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ - type: 'kibana_context', timeRange, query, filters, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx index 6fcf2bab8921f..117b2a3a949ea 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx @@ -8,15 +8,15 @@ import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Query, TimeRange, esFilters } from 'src/plugins/data/public'; -import { ExpressionRenderer } from 'src/plugins/expressions/public'; import { IIndexPattern } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; +import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { Embeddable as AbstractEmbeddable, EmbeddableOutput, IContainer, EmbeddableInput, -} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +} from '../../../../../../../src/plugins/embeddable/public'; import { Document, DOC_TYPE } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; @@ -40,7 +40,7 @@ export interface LensEmbeddableOutput extends EmbeddableOutput { export class Embeddable extends AbstractEmbeddable { type = DOC_TYPE; - private expressionRenderer: ExpressionRenderer; + private expressionRenderer: ReactExpressionRendererType; private savedVis: Document; private domNode: HTMLElement | Element | undefined; private subscription: Subscription; @@ -53,7 +53,7 @@ export class Embeddable extends AbstractEmbeddable
{error}
} />
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts index ffb8be1deaa9e..9368674de31c5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { mergeTables } from './merge_tables'; -import { KibanaDatatable } from 'src/plugins/expressions/public'; +import { KibanaDatatable } from 'src/plugins/expressions'; jest.mock('ui/new_platform'); @@ -40,7 +40,8 @@ describe('lens_merge_tables', () => { mergeTables.fn( null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, - {} + // eslint-disable-next-line + {} as any ) ).toEqual({ tables: { first: sampleTable1, second: sampleTable2 }, @@ -59,7 +60,8 @@ describe('lens_merge_tables', () => { }, }, { layerIds: ['first', 'second'], tables: [] }, - {} + // eslint-disable-next-line + {} as any ) ).toMatchInlineSnapshot(` Object { @@ -83,7 +85,8 @@ describe('lens_merge_tables', () => { }, }, { layerIds: ['first', 'second'], tables: [] }, - {} + // eslint-disable-next-line + {} as any ); expect( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index dc03be894a87c..3c466522e1ebe 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -5,7 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaContext, KibanaDatatable } from 'src/plugins/expressions/public'; +import { + ExpressionFunctionDefinition, + ExpressionValueSearchContext, + KibanaDatatable, +} from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; @@ -14,9 +18,9 @@ interface MergeTables { tables: KibanaDatatable[]; } -export const mergeTables: ExpressionFunction< +export const mergeTables: ExpressionFunctionDefinition< 'lens_merge_tables', - KibanaContext | null, + ExpressionValueSearchContext | null, MergeTables, LensMultiTable > = { @@ -37,10 +41,8 @@ export const mergeTables: ExpressionFunction< multi: true, }, }, - context: { - types: ['kibana_context', 'null'], - }, - fn(ctx, { layerIds, tables }: MergeTables) { + inputTypes: ['kibana_context', 'null'], + fn(input, { layerIds, tables }) { const resultTables: Record = {}; tables.forEach((table, index) => { resultTables[layerIds[index]] = table; @@ -48,17 +50,17 @@ export const mergeTables: ExpressionFunction< return { type: 'lens_multitable', tables: resultTables, - dateRange: getDateRange(ctx), + dateRange: getDateRange(input), }; }, }; -function getDateRange(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { +function getDateRange(value?: ExpressionValueSearchContext | null) { + if (!value || !value.timeRange) { return; } - const dateRange = toAbsoluteDates({ fromDate: ctx.timeRange.from, toDate: ctx.timeRange.to }); + const dateRange = toAbsoluteDates({ fromDate: value.timeRange.from, toDate: value.timeRange.to }); if (!dateRange) { return; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 7257647d5953e..b4fc88cb074c7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { - ExpressionRendererProps, + ReactExpressionRendererProps, ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; @@ -98,7 +98,7 @@ export type MockedStartDependencies = Omit { return jest.fn(_ => ); } 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 7546ac6509913..e914eb7d7784b 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 @@ -71,7 +71,7 @@ export class EditorFramePlugin { 'lens', new EmbeddableFactory( plugins.chrome, - plugins.expressions.ExpressionRenderer, + plugins.expressions.ReactExpressionRenderer, plugins.data.indexPatterns ) ); @@ -96,7 +96,7 @@ export class EditorFramePlugin { (doc && doc.visualizationType) || firstVisualizationId || null } core={core} - ExpressionRenderer={plugins.expressions.ExpressionRenderer} + ExpressionRenderer={plugins.expressions.ReactExpressionRenderer} doc={doc} dateRange={dateRange} query={query} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts index 8146bc39ef82e..6611c1a227442 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts @@ -18,7 +18,8 @@ describe('auto_date', () => { { aggConfigs: 'canttouchthis', }, - {} + // eslint-disable-next-line + {} as any ); expect(result).toEqual('canttouchthis'); @@ -40,7 +41,8 @@ describe('auto_date', () => { { aggConfigs, }, - {} + // eslint-disable-next-line + {} as any ); expect(result).toEqual(aggConfigs); @@ -62,7 +64,8 @@ describe('auto_date', () => { { aggConfigs, }, - {} + // eslint-disable-next-line + {} as any ); const interval = JSON.parse(result).find( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts index 7720af8ee9001..be7929392635f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts @@ -7,7 +7,7 @@ import { TimeBuckets } from 'ui/time_buckets'; import dateMath from '@elastic/datemath'; import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaContext, } from '../../../../../../src/plugins/expressions/public'; import { DateRange } from '../../../../../plugins/lens/common'; @@ -69,7 +69,7 @@ function autoIntervalFromContext(ctx?: KibanaContext | null) { * This allows us to support 'auto' on all date fields, and opens the * door to future customizations (e.g. adjusting the level of detail, etc). */ -export const autoDate: ExpressionFunction< +export const autoDate: ExpressionFunctionDefinition< 'lens_auto_date', KibanaContext | null, LensAutoDateProps, @@ -78,9 +78,7 @@ export const autoDate: ExpressionFunction< name: 'lens_auto_date', aliases: [], help: '', - context: { - types: ['kibana_context', 'null'], - }, + inputTypes: ['kibana_context', 'null'], args: { aggConfigs: { types: ['string'], @@ -88,8 +86,8 @@ export const autoDate: ExpressionFunction< help: '', }, }, - fn(ctx: KibanaContext, args: LensAutoDateProps) { - const interval = autoIntervalFromContext(ctx); + fn(input, args) { + const interval = autoIntervalFromContext(input); if (!interval) { return args.aggConfigs; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts index a231374b89a42..9da7591305a6c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts @@ -6,6 +6,7 @@ import { renameColumns } from './rename_columns'; import { KibanaDatatable } from '../../../../../../src/plugins/expressions/public'; +import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { it('should rename columns of a given datatable', () => { @@ -34,7 +35,13 @@ describe('rename_columns', () => { }, }; - expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` + const result = renameColumns.fn( + input, + { idMap: JSON.stringify(idMap) }, + createMockExecutionContext() + ); + + expect(result).toMatchInlineSnapshot(` Object { "columns": Array [ Object { @@ -83,9 +90,13 @@ describe('rename_columns', () => { }, }; - expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {}).rows[0].a).toEqual( - '(empty)' + const result = renameColumns.fn( + input, + { idMap: JSON.stringify(idMap) }, + createMockExecutionContext() ); + + expect(result.rows[0].a).toEqual('(empty)'); }); it('should keep columns which are not mapped', () => { @@ -107,7 +118,13 @@ describe('rename_columns', () => { b: { id: 'c', label: 'Catamaran' }, }; - expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` + const result = renameColumns.fn( + input, + { idMap: JSON.stringify(idMap) }, + createMockExecutionContext() + ); + + expect(result).toMatchInlineSnapshot(` Object { "columns": Array [ Object { @@ -161,7 +178,13 @@ describe('rename_columns', () => { b: { id: 'c', label: 'Apple', operationType: 'date_histogram', sourceField: 'banana' }, }; - expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` + const result = renameColumns.fn( + input, + { idMap: JSON.stringify(idMap) }, + createMockExecutionContext() + ); + + expect(result).toMatchInlineSnapshot(` Object { "columns": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts index 19dd661409c6f..248eb12ec8026 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; import { - ExpressionFunction, + ExpressionFunctionDefinition, KibanaDatatable, KibanaDatatableColumn, -} from 'src/plugins/expressions/common'; +} from 'src/plugins/expressions'; import { IndexPatternColumn } from './operations'; interface RemapArgs { @@ -18,7 +18,7 @@ interface RemapArgs { export type OriginalColumn = { id: string } & IndexPatternColumn; -export const renameColumns: ExpressionFunction< +export const renameColumns: ExpressionFunctionDefinition< 'lens_rename_columns', KibanaDatatable, RemapArgs, @@ -38,10 +38,8 @@ export const renameColumns: ExpressionFunction< }), }, }, - context: { - types: ['kibana_datatable'], - }, - fn(data: KibanaDatatable, { idMap: encodedIdMap }: RemapArgs) { + inputTypes: ['kibana_datatable'], + fn(data, { idMap: encodedIdMap }) { const idMap = JSON.parse(encodedIdMap) as Record; return { 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 1e0fce9f538b4..3da38d486aecd 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,6 +9,7 @@ import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { MetricConfig } from './types'; +import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; import { IFieldFormat } from '../../../../../../src/plugins/data/public'; function sampleArgs() { @@ -41,8 +42,9 @@ describe('metric_expression', () => { describe('metricChart', () => { test('it renders with the specified data and args', () => { const { data, args } = sampleArgs(); + const result = metricChart.fn(data, args, createMockExecutionContext()); - expect(metricChart.fn(data, args, {})).toEqual({ + expect(result).toEqual({ type: 'render', as: 'lens_metric_chart_renderer', value: { data, args }, diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index 7fb44a3a37c51..66ed963002f59 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -8,8 +8,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { - ExpressionFunction, - IInterpreterRenderFunction, + ExpressionFunctionDefinition, + ExpressionRenderDefinition, IInterpreterRenderHandlers, } from '../../../../../../src/plugins/expressions/public'; import { MetricConfig } from './types'; @@ -28,12 +28,12 @@ export interface MetricRender { value: MetricChartProps; } -export const metricChart: ExpressionFunction< +export const metricChart: ExpressionFunctionDefinition< 'lens_metric_chart', LensMultiTable, - MetricConfig, + Omit, MetricRender -> = ({ +> = { name: 'lens_metric_chart', type: 'render', help: 'A metric chart', @@ -54,10 +54,8 @@ export const metricChart: ExpressionFunction< 'The display mode of the chart - reduced will only show the metric itself without min size', }, }, - context: { - types: ['lens_multitable'], - }, - fn(data: LensMultiTable, args: MetricChartProps) { + inputTypes: ['lens_multitable'], + fn(data, args) { return { type: 'render', as: 'lens_metric_chart_renderer', @@ -65,23 +63,17 @@ export const metricChart: ExpressionFunction< data, args, }, - }; + } as MetricRender; }, - // TODO the typings currently don't support custom type args. As soon as they do, this can be removed -} as unknown) as ExpressionFunction< - 'lens_metric_chart', - LensMultiTable, - MetricConfig, - MetricRender ->; +}; export const getMetricChartRenderer = ( formatFactory: FormatFactory -): IInterpreterRenderFunction => ({ +): ExpressionRenderDefinition => ({ name: 'lens_metric_chart_renderer', displayName: 'Metric chart', help: 'Metric chart renderer', - validate: () => {}, + validate: () => undefined, reuseDomNode: true, render: (domNode: Element, config: MetricChartProps, handlers: IInterpreterRenderHandlers) => { ReactDOM.render(, domNode, () => { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx index f0603f021c452..6feece99370ef 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx @@ -49,7 +49,7 @@ class XyVisualizationPlugin { expressions.registerFunction(() => layerConfig); expressions.registerFunction(() => xyChart); - expressions.registerRenderer(() => + expressions.registerRenderer( getXyChartRenderer({ formatFactory, timeZone: getTimeZone(getUiSettingsClient()), diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 6dcd19f1493f2..b49e6fa6b4b6f 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -6,7 +6,7 @@ import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, ArgumentType } from 'src/plugins/expressions/common'; +import { ArgumentType, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import chartAreaSVG from '../assets/chart_area.svg'; import chartAreaStackedSVG from '../assets/chart_area_stacked.svg'; import chartBarSVG from '../assets/chart_bar.svg'; @@ -24,7 +24,7 @@ export interface LegendConfig { type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' }; -export const legendConfig: ExpressionFunction< +export const legendConfig: ExpressionFunctionDefinition< 'lens_xy_legendConfig', null, LegendConfig, @@ -34,9 +34,7 @@ export const legendConfig: ExpressionFunction< aliases: [], type: 'lens_xy_legendConfig', help: `Configure the xy chart's legend`, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { isVisible: { types: ['boolean'], @@ -52,7 +50,7 @@ export const legendConfig: ExpressionFunction< }), }, }, - fn: function fn(_context: unknown, args: LegendConfig) { + fn: function fn(input: unknown, args: LegendConfig) { return { type: 'lens_xy_legendConfig', ...args, @@ -89,14 +87,17 @@ export interface XConfig extends AxisConfig { type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; -export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConfigResult> = { +export const xConfig: ExpressionFunctionDefinition< + 'lens_xy_xConfig', + null, + XConfig, + XConfigResult +> = { name: 'lens_xy_xConfig', aliases: [], type: 'lens_xy_xConfig', help: `Configure the xy chart's x axis`, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { ...axisConfig, accessor: { @@ -104,7 +105,7 @@ export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConf help: 'The column to display on the x axis.', }, }, - fn: function fn(_context: unknown, args: XConfig) { + fn: function fn(input: unknown, args: XConfig) { return { type: 'lens_xy_xConfig', ...args, @@ -114,7 +115,7 @@ export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConf type LayerConfigResult = LayerArgs & { type: 'lens_xy_layer' }; -export const layerConfig: ExpressionFunction< +export const layerConfig: ExpressionFunctionDefinition< 'lens_xy_layer', null, LayerArgs, @@ -124,9 +125,7 @@ export const layerConfig: ExpressionFunction< aliases: [], type: 'lens_xy_layer', help: `Configure a layer in the xy chart`, - context: { - types: ['null'], - }, + inputTypes: ['null'], args: { ...axisConfig, layerId: { @@ -172,7 +171,7 @@ export const layerConfig: ExpressionFunction< help: 'JSON key-value pairs of column ID to label', }, }, - fn: function fn(_context: unknown, args: LayerArgs) { + fn: function fn(input: unknown, args: LayerArgs) { return { type: 'lens_xy_layer', ...args, @@ -209,7 +208,7 @@ export type LayerArgs = LayerConfig & { export interface XYArgs { xTitle: string; yTitle: string; - legend: LegendConfig; + legend: LegendConfig & { type: 'lens_xy_legendConfig' }; layers: LayerArgs[]; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index 878db1fe9a458..daedb30db3f3e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -11,6 +11,7 @@ import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types'; +import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; function sampleArgs() { const data: LensMultiTable = { @@ -40,6 +41,7 @@ function sampleArgs() { xTitle: '', yTitle: '', legend: { + type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top, }, @@ -69,7 +71,9 @@ describe('xy_expression', () => { position: Position.Left, }; - expect(legendConfig.fn(null, args, {})).toEqual({ + const result = legendConfig.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'lens_xy_legendConfig', ...args, }); @@ -87,7 +91,9 @@ describe('xy_expression', () => { isHistogram: false, }; - expect(layerConfig.fn(null, args, {})).toEqual({ + const result = layerConfig.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'lens_xy_layer', ...args, }); @@ -97,8 +103,9 @@ describe('xy_expression', () => { describe('xyChart', () => { test('it renders with the specified data and args', () => { const { data, args } = sampleArgs(); + const result = xyChart.fn(data, args, createMockExecutionContext()); - expect(xyChart.fn(data, args, {})).toEqual({ + expect(result).toEqual({ type: 'render', as: 'lens_xy_chart_renderer', value: { data, args }, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 32c1ace5b1770..c62a8288d6655 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -18,10 +18,11 @@ import { } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { - ExpressionFunction, KibanaDatatable, IInterpreterRenderHandlers, - IInterpreterRenderFunction, + ExpressionRenderDefinition, + ExpressionFunctionDefinition, + ExpressionValueSearchContext, } from 'src/plugins/expressions/public'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,9 +53,15 @@ type XYChartRenderProps = XYChartProps & { timeZone: string; }; -export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender> = ({ +export const xyChart: ExpressionFunctionDefinition< + 'lens_xy_chart', + LensMultiTable | ExpressionValueSearchContext | null, + XYArgs, + XYRender +> = { name: 'lens_xy_chart', type: 'render', + inputTypes: ['lens_multitable', 'kibana_context', 'null'], help: i18n.translate('xpack.lens.xyChart.help', { defaultMessage: 'An X/Y chart', }), @@ -74,14 +81,12 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs }), }, layers: { - types: ['lens_xy_layer'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + types: ['lens_xy_layer'] as any, help: 'Layers of visual series', multi: true, }, }, - context: { - types: ['lens_multitable', 'kibana_context', 'null'], - }, fn(data: LensMultiTable, args: XYArgs) { return { type: 'render', @@ -92,19 +97,18 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs }, }; }, - // TODO the typings currently don't support custom type args. As soon as they do, this can be removed -} as unknown) as ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender>; +}; export const getXyChartRenderer = (dependencies: { formatFactory: FormatFactory; timeZone: string; -}): IInterpreterRenderFunction => ({ +}): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', displayName: 'XY chart', help: i18n.translate('xpack.lens.xyChart.renderer.help', { defaultMessage: 'X/Y chart renderer', }), - validate: () => {}, + validate: () => undefined, reuseDomNode: true, render: (domNode: Element, config: XYChartProps, handlers: IInterpreterRenderHandlers) => { handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); diff --git a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts deleted file mode 100644 index 48e88e79f9674..0000000000000 --- a/x-pack/legacy/plugins/ml/common/constants/feature_flags.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. - */ - -// This flag is used on the server side as the default setting. -// Plugin initialization does some additional integrity checks and tests if the necessary -// indices and aliases exist. Based on that the final setting will be available -// as an injectedVar on the client side and can be accessed like: -// - -export const FEATURE_ANNOTATIONS_ENABLED = true; diff --git a/x-pack/legacy/plugins/ml/common/types/jobs.ts b/x-pack/legacy/plugins/ml/common/types/jobs.ts index 47f34f6568eed..a9885048550bb 100644 --- a/x-pack/legacy/plugins/ml/common/types/jobs.ts +++ b/x-pack/legacy/plugins/ml/common/types/jobs.ts @@ -20,7 +20,10 @@ export interface MlJob { }; create_time: number; custom_settings: object; - data_counts: object; + data_counts: { + earliest_record_timestamp: number; + latest_record_timestamp: number; + }; data_description: { time_field: string; time_format: string; diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index cd6395500a804..3e1a2cf9ab2e6 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -11,16 +11,25 @@ export interface ModuleJob { config: Omit; } +export interface ModuleDataFeed { + id: string; + config: Omit; +} + export interface KibanaObjectConfig extends SavedObjectAttributes { description: string; title: string; version: number; + kibanaSavedObjectMeta?: { + searchSourceJSON: string; + }; } export interface KibanaObject { id: string; title: string; config: KibanaObjectConfig; + exists?: boolean; } export interface KibanaObjects { @@ -39,14 +48,18 @@ export interface Module { defaultIndexPattern: string; query: any; jobs: ModuleJob[]; - datafeeds: Datafeed[]; + datafeeds: ModuleDataFeed[]; kibana: KibanaObjects; } -export interface KibanaObjectResponse { - exists?: boolean; - success?: boolean; +export interface ResultItem { id: string; + success?: boolean; +} + +export interface KibanaObjectResponse extends ResultItem { + exists?: boolean; + error?: any; } export interface SetupError { @@ -58,16 +71,12 @@ export interface SetupError { statusCode: number; } -export interface DatafeedResponse { - id: string; - success: boolean; +export interface DatafeedResponse extends ResultItem { started: boolean; error?: SetupError; } -export interface JobResponse { - id: string; - success: boolean; +export interface JobResponse extends ResultItem { error?: SetupError; } @@ -75,10 +84,14 @@ export interface DataRecognizerConfigResponse { datafeeds: DatafeedResponse[]; jobs: JobResponse[]; kibana: { - search: KibanaObjectResponse; - visualization: KibanaObjectResponse; - dashboard: KibanaObjectResponse; + search: KibanaObjectResponse[]; + visualization: KibanaObjectResponse[]; + dashboard: KibanaObjectResponse[]; }; } +export type GeneralOverride = any; + export type JobOverride = Partial; + +export type DatafeedOverride = Partial; 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 cfff15bb97be2..7dcd4b20fe0bf 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 @@ -45,3 +45,10 @@ export function mlFunctionToESAggregation(functionName: string): string | null; export function isModelPlotEnabled(job: Job, detectorIndex: number, entityFields: any[]): boolean; export function getSafeAggregationName(fieldName: string, index: number): string; + +export function getLatestDataOrBucketTimestamp( + latestDataTimestamp: number, + latestBucketTimestamp: number +): number; + +export function prefixDatafeedId(datafeedId: string, prefix: string): string; diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index fc1cec7c16208..0ef5e14e44f71 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -45,7 +45,6 @@ export const ml = (kibana: any) => { category: DEFAULT_APP_CATEGORIES.analyze, }, styleSheetPaths: resolve(__dirname, 'public/application/index.scss'), - hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { isNamespaceAgnostic: true, @@ -87,7 +86,7 @@ export const ml = (kibana: any) => { const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; const plugins = { elasticsearch: server.plugins.elasticsearch, // legacy - security: server.plugins.security, + security: server.newPlatform.setup.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, home, diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 085e395f2ebf7..24cbfbfb346dd 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -7,50 +7,78 @@ import React, { FC } from 'react'; import ReactDOM from 'react-dom'; -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; - // needed to make syntax highlighting work in ace editors import 'ace'; import { AppMountParameters, CoreStart } from 'kibana/public'; -import { - IndexPatternsContract, - Plugin as DataPlugin, -} from '../../../../../../src/plugins/data/public'; -import { KibanaConfigTypeFix } from './contexts/kibana'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; + +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { setDependencyCache, clearCache } from './util/dependency_cache'; import { MlRouter } from './routing'; export interface MlDependencies extends AppMountParameters { - npData: ReturnType; - indexPatterns: IndexPatternsContract; + data: DataPublicPluginStart; + __LEGACY: { + XSRF: string; + APP_URL: string; + }; } interface AppProps { coreStart: CoreStart; - indexPatterns: IndexPatternsContract; + deps: MlDependencies; } -const App: FC = ({ coreStart, indexPatterns }) => { - const config = (coreStart.uiSettings as never) as KibanaConfigTypeFix; // TODO - make this UiSettingsClientContract, get rid of KibanaConfigTypeFix +const App: FC = ({ coreStart, deps }) => { + setDependencyCache({ + indexPatterns: deps.data.indexPatterns, + timefilter: deps.data.query.timefilter, + config: coreStart.uiSettings!, + chrome: coreStart.chrome!, + docLinks: coreStart.docLinks!, + toastNotifications: coreStart.notifications.toasts, + overlays: coreStart.overlays, + recentlyAccessed: coreStart.chrome!.recentlyAccessed, + fieldFormats: deps.data.fieldFormats, + autocomplete: deps.data.autocomplete, + basePath: coreStart.http.basePath, + savedObjectsClient: coreStart.savedObjects.client, + XSRF: deps.__LEGACY.XSRF, + APP_URL: deps.__LEGACY.APP_URL, + application: coreStart.application, + http: coreStart.http, + }); + deps.onAppLeave(actions => { + clearCache(); + return actions.default(); + }); + + const pageDeps = { + indexPatterns: deps.data.indexPatterns, + config: coreStart.uiSettings!, + setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, + }; + + const services = { + appName: 'ML', + data: deps.data, + ...coreStart, + }; + const I18nContext = coreStart.i18n.Context; return ( - + + + + + ); }; -export const renderApp = ( - coreStart: CoreStart, - depsStart: object, - { element, indexPatterns }: MlDependencies -) => { - ReactDOM.render(, element); +export const renderApp = (coreStart: CoreStart, depsStart: object, deps: MlDependencies) => { + ReactDOM.render(, deps.element); - return () => ReactDOM.unmountComponentAtNode(element); + return () => ReactDOM.unmountComponentAtNode(deps.element); }; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx index 323de6d3a8dd5..2568a6f40d326 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx @@ -21,12 +21,7 @@ describe('AnnotationDescriptionList', () => { }); test('Initialization with annotation.', () => { - const wrapper = shallowWithIntl( - - ); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); 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 index 3d98e2d66935c..cf8fd299c07d7 100644 --- 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 @@ -13,27 +13,24 @@ import React from 'react'; import { EuiDescriptionList } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; 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) => { +export const AnnotationDescriptionList = ({ annotation }: Props) => { const listItems = [ { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', { defaultMessage: 'Job ID', }), description: annotation.job_id, }, { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', { defaultMessage: 'Start', }), description: formatHumanReadableDateTimeSeconds(annotation.timestamp), @@ -42,8 +39,7 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props if (annotation.end_timestamp !== undefined) { listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', { defaultMessage: 'End', }), description: formatHumanReadableDateTimeSeconds(annotation.end_timestamp), @@ -52,31 +48,36 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props if (annotation.create_time !== undefined && annotation.modified_time !== undefined) { listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle', + title: i18n.translate('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', - }), + title: i18n.translate( + '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', - }), + title: i18n.translate( + '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', - }), + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle', + { + defaultMessage: 'Modified by', + } + ), description: annotation.modified_username, }); } @@ -88,4 +89,4 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props listItems={listItems} /> ); -}); +}; 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 index 6668518822710..65fe36a7b611b 100644 --- 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 @@ -27,7 +27,6 @@ import { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../../common/constants/annotations'; import { annotation$, @@ -38,6 +37,7 @@ import { AnnotationDescriptionList } from '../annotation_description_list'; import { DeleteAnnotationModal } from '../delete_annotation_modal'; import { ml } from '../../../services/ml_api_service'; +import { getToastNotifications } from '../../../util/dependency_cache'; interface Props { annotation: AnnotationState; @@ -47,7 +47,7 @@ interface State { isDeleteModalVisible: boolean; } -class AnnotationFlyoutIntl extends Component { +class AnnotationFlyoutUI extends Component { public state: State = { isDeleteModalVisible: false, }; @@ -75,6 +75,7 @@ class AnnotationFlyoutIntl extends Component { public deleteHandler = async () => { const { annotation } = this.props; + const toastNotifications = getToastNotifications(); if (annotation === null) { return; @@ -161,6 +162,7 @@ class AnnotationFlyoutIntl extends Component { .indexAnnotation(annotation) .then(() => { annotationsRefreshed(); + const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addSuccess( i18n.translate( @@ -184,6 +186,7 @@ class AnnotationFlyoutIntl extends Component { } }) .catch(resp => { + const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addDanger( i18n.translate( @@ -343,5 +346,5 @@ export const AnnotationFlyout: FC = props => { return null; } - return ; + return ; }; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 3329bf1aab64a..d9c32be41cd72 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -27,6 +27,8 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; @@ -48,458 +50,439 @@ import { annotationsRefreshed, } from '../../../services/annotations_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; - const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of annotations for an ML job. */ -const AnnotationsTable = injectI18n( - class AnnotationsTable extends Component { - static propTypes = { - annotations: PropTypes.array, - jobs: PropTypes.array, - isSingleMetricViewerLinkVisible: PropTypes.bool, - isNumberBadgeVisible: PropTypes.bool, +export class AnnotationsTable extends Component { + static propTypes = { + annotations: PropTypes.array, + jobs: PropTypes.array, + isSingleMetricViewerLinkVisible: PropTypes.bool, + isNumberBadgeVisible: PropTypes.bool, + }; + + constructor(props) { + super(props); + this.state = { + annotations: [], + isLoading: false, + jobId: + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 && + this.props.jobs[0] !== undefined + ? this.props.jobs[0].job_id + : undefined, }; + } - constructor(props) { - super(props); - this.state = { - annotations: [], - isLoading: false, - // Need to do a detailed check here because the angular wrapper could pass on something like `[undefined]`. - jobId: - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 && - this.props.jobs[0] !== undefined - ? this.props.jobs[0].job_id - : undefined, - }; - } + getAnnotations() { + const job = this.props.jobs[0]; + const dataCounts = job.data_counts; - getAnnotations() { - const job = this.props.jobs[0]; - const dataCounts = job.data_counts; + this.setState({ + isLoading: true, + }); - this.setState({ - isLoading: true, - }); - - if (dataCounts.processed_record_count > 0) { - // Load annotations for the selected job. - ml.annotations - .getAnnotations({ - jobIds: [job.job_id], - earliestMs: null, - latestMs: null, - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - }) - .toPromise() - .then(resp => { - this.setState((prevState, props) => ({ - annotations: resp.annotations[props.jobs[0].job_id] || [], - errorMessage: undefined, - isLoading: false, - jobId: props.jobs[0].job_id, - })); - }) - .catch(resp => { - console.log('Error loading list of annotations for jobs list:', resp); - this.setState({ - annotations: [], - errorMessage: 'Error loading the list of annotations for this job', - isLoading: false, - jobId: undefined, - }); + if (dataCounts.processed_record_count > 0) { + // Load annotations for the selected job. + ml.annotations + .getAnnotations({ + jobIds: [job.job_id], + earliestMs: null, + latestMs: null, + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .toPromise() + .then(resp => { + this.setState((prevState, props) => ({ + annotations: resp.annotations[props.jobs[0].job_id] || [], + errorMessage: undefined, + isLoading: false, + jobId: props.jobs[0].job_id, + })); + }) + .catch(resp => { + console.log('Error loading list of annotations for jobs list:', resp); + this.setState({ + annotations: [], + errorMessage: 'Error loading the list of annotations for this job', + isLoading: false, + jobId: undefined, }); - } + }); } + } - getJob(jobId) { - // check if the job was supplied via props and matches the supplied jobId - if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) { - const job = this.props.jobs[0]; - if (jobId === undefined || job.job_id === jobId) { - return job; - } + getJob(jobId) { + // check if the job was supplied via props and matches the supplied jobId + if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) { + const job = this.props.jobs[0]; + if (jobId === undefined || job.job_id === jobId) { + return job; } - - return mlJobService.getJob(jobId); } - annotationsRefreshSubscription = null; + return mlJobService.getJob(jobId); + } - componentDidMount() { - if ( - this.props.annotations === undefined && - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 - ) { - this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => - this.getAnnotations() - ); - annotationsRefreshed(); - } + annotationsRefreshSubscription = null; + + componentDidMount() { + if ( + this.props.annotations === undefined && + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 + ) { + this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => + this.getAnnotations() + ); + annotationsRefreshed(); } + } - previousJobId = undefined; - componentDidUpdate() { - if ( - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 && - this.previousJobId !== this.props.jobs[0].job_id && - this.props.annotations === undefined && - this.state.isLoading === false && - this.state.jobId !== this.props.jobs[0].job_id - ) { - annotationsRefreshed(); - this.previousJobId = this.props.jobs[0].job_id; - } + previousJobId = undefined; + componentDidUpdate() { + if ( + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 && + this.previousJobId !== this.props.jobs[0].job_id && + this.props.annotations === undefined && + this.state.isLoading === false && + this.state.jobId !== this.props.jobs[0].job_id + ) { + annotationsRefreshed(); + this.previousJobId = this.props.jobs[0].job_id; } + } - componentWillUnmount() { - if (this.annotationsRefreshSubscription !== null) { - this.annotationsRefreshSubscription.unsubscribe(); - } + componentWillUnmount() { + if (this.annotationsRefreshSubscription !== null) { + this.annotationsRefreshSubscription.unsubscribe(); } + } - openSingleMetricView = (annotation = {}) => { - // Creates the link to the Single Metric Viewer. - // Set the total time range from the start to the end of the annotation. - const job = this.getJob(annotation.job_id); - const dataCounts = job.data_counts; - const resultLatest = getLatestDataOrBucketTimestamp( - dataCounts.latest_record_timestamp, - dataCounts.latest_bucket_timestamp - ); - const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); - const to = new Date(resultLatest).toISOString(); + openSingleMetricView = (annotation = {}) => { + // Creates the link to the Single Metric Viewer. + // Set the total time range from the start to the end of the annotation. + const job = this.getJob(annotation.job_id); + const dataCounts = job.data_counts; + const resultLatest = getLatestDataOrBucketTimestamp( + dataCounts.latest_record_timestamp, + dataCounts.latest_bucket_timestamp + ); + const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); + const to = new Date(resultLatest).toISOString(); + + const globalSettings = { + ml: { + jobIds: [job.job_id], + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + time: { + from, + to, + mode: 'absolute', + }, + }; - const globalSettings = { - ml: { - jobIds: [job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + const appState = { + query: { + query_string: { + analyze_wildcard: true, + query: '*', }, - time: { - from, - to, - mode: 'absolute', - }, - }; + }, + }; - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, + if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { + appState.mlTimeSeriesExplorer = { + zoom: { + from: new Date(annotation.timestamp).toISOString(), + to: new Date(annotation.end_timestamp).toISOString(), }, }; - if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { - appState.mlTimeSeriesExplorer = { - zoom: { - from: new Date(annotation.timestamp).toISOString(), - to: new Date(annotation.end_timestamp).toISOString(), - }, - }; - - if (annotation.timestamp < dataCounts.earliest_record_timestamp) { - globalSettings.time.from = new Date(annotation.timestamp).toISOString(); - } - - if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { - globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); - } + if (annotation.timestamp < dataCounts.earliest_record_timestamp) { + globalSettings.time.from = new Date(annotation.timestamp).toISOString(); } - const _g = rison.encode(globalSettings); - const _a = rison.encode(appState); + if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { + globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); + } + } - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); - }; + const _g = rison.encode(globalSettings); + const _a = rison.encode(appState); - onMouseOverRow = record => { - if (this.mouseOverRecord !== undefined) { - if (this.mouseOverRecord.rowId !== record.rowId) { - // Mouse is over a different row, fire mouseleave on the previous record. - mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); - - // fire mouseenter on the new record. - mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); - } - } else { - // Mouse is now over a row, fire mouseenter on the record. + const url = `?_g=${_g}&_a=${_a}`; + addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); + window.open(`#/timeseriesexplorer${url}`, '_self'); + }; + + onMouseOverRow = record => { + if (this.mouseOverRecord !== undefined) { + if (this.mouseOverRecord.rowId !== record.rowId) { + // Mouse is over a different row, fire mouseleave on the previous record. + mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); + + // fire mouseenter on the new record. mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); } + } else { + // Mouse is now over a row, fire mouseenter on the record. + mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); + } - this.mouseOverRecord = record; - }; + this.mouseOverRecord = record; + }; - onMouseLeaveRow = () => { - if (this.mouseOverRecord !== undefined) { - mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); - this.mouseOverRecord = undefined; - } - }; + onMouseLeaveRow = () => { + if (this.mouseOverRecord !== undefined) { + mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); + this.mouseOverRecord = undefined; + } + }; - render() { - const { - isSingleMetricViewerLinkVisible = true, - isNumberBadgeVisible = false, - intl, - } = this.props; + render() { + const { isSingleMetricViewerLinkVisible = true, isNumberBadgeVisible = false } = this.props; - if (this.props.annotations === undefined) { - if (this.state.isLoading === true) { - return ( - - - - - - ); - } + if (this.props.annotations === undefined) { + if (this.state.isLoading === true) { + return ( + + + + + + ); + } - if (this.state.errorMessage !== undefined) { - return ; - } + if (this.state.errorMessage !== undefined) { + return ; } + } - const annotations = this.props.annotations || this.state.annotations; + const annotations = this.props.annotations || this.state.annotations; - if (annotations.length === 0) { - return ( - + } + iconType="iInCircle" + role="alert" + > + {this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) && ( +

this.openSingleMetricView()}> + + + ), + }} /> - } - iconType="iInCircle" - role="alert" - > - {this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) && ( -

- this.openSingleMetricView()}> - - - ), - }} - /> -

- )} -
- ); - } +

+ )} +
+ ); + } - function renderDate(date) { - return formatDate(date, TIME_FORMAT); - } + function renderDate(date) { + return formatDate(date, TIME_FORMAT); + } - const columns = [ - { - field: 'annotation', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.annotationColumnName', - defaultMessage: 'Annotation', - }), - sortable: true, - width: '50%', - scope: 'row', - }, - { - field: 'timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.fromColumnName', - defaultMessage: 'From', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'end_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.toColumnName', - defaultMessage: 'To', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'modified_time', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.lastModifiedDateColumnName', - defaultMessage: 'Last modified date', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'modified_username', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.lastModifiedByColumnName', - defaultMessage: 'Last modified by', - }), - sortable: true, + const columns = [ + { + field: 'annotation', + name: i18n.translate('xpack.ml.annotationsTable.annotationColumnName', { + defaultMessage: 'Annotation', + }), + sortable: true, + width: '50%', + scope: 'row', + }, + { + field: 'timestamp', + name: i18n.translate('xpack.ml.annotationsTable.fromColumnName', { + defaultMessage: 'From', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'end_timestamp', + name: i18n.translate('xpack.ml.annotationsTable.toColumnName', { + defaultMessage: 'To', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'modified_time', + name: i18n.translate('xpack.ml.annotationsTable.lastModifiedDateColumnName', { + defaultMessage: 'Last modified date', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'modified_username', + name: i18n.translate('xpack.ml.annotationsTable.lastModifiedByColumnName', { + defaultMessage: 'Last modified by', + }), + sortable: true, + }, + ]; + + const jobIds = _.uniq(annotations.map(a => a.job_id)); + if (jobIds.length > 1) { + columns.unshift({ + field: 'job_id', + name: i18n.translate('xpack.ml.annotationsTable.jobIdColumnName', { + defaultMessage: 'job ID', + }), + sortable: true, + }); + } + + if (isNumberBadgeVisible) { + columns.unshift({ + field: 'key', + name: i18n.translate('xpack.ml.annotationsTable.labelColumnName', { + defaultMessage: 'Label', + }), + sortable: true, + width: '60px', + render: key => { + return {key}; }, - ]; - - const jobIds = _.uniq(annotations.map(a => a.job_id)); - if (jobIds.length > 1) { - columns.unshift({ - field: 'job_id', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.jobIdColumnName', - defaultMessage: 'job ID', - }), - sortable: true, - }); - } + }); + } - if (isNumberBadgeVisible) { - columns.unshift({ - field: 'key', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.labelColumnName', - defaultMessage: 'Label', - }), - sortable: true, - width: '60px', - render: key => { - return {key}; - }, - }); - } + const actions = []; - const actions = []; + actions.push({ + render: annotation => { + const editAnnotationsTooltipText = ( + + ); + const editAnnotationsTooltipAriaLabelText = ( + + ); + return ( + + annotation$.next(annotation)} + iconType="pencil" + aria-label={editAnnotationsTooltipAriaLabelText} + /> + + ); + }, + }); + if (isSingleMetricViewerLinkVisible) { actions.push({ render: annotation => { - const editAnnotationsTooltipText = ( + const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); + const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( + + ) : ( ); - const editAnnotationsTooltipAriaLabelText = ( + const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? ( + ) : ( + ); + return ( - + annotation$.next(annotation)} - iconType="pencil" - aria-label={editAnnotationsTooltipAriaLabelText} + onClick={() => this.openSingleMetricView(annotation)} + disabled={!isDrillDownAvailable} + iconType="stats" + aria-label={openInSingleMetricViewerAriaLabelText} /> ); }, }); + } - if (isSingleMetricViewerLinkVisible) { - actions.push({ - render: annotation => { - const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); - const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( - - ) : ( - - ); - const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? ( - - ) : ( - - ); - - return ( - - this.openSingleMetricView(annotation)} - disabled={!isDrillDownAvailable} - iconType="stats" - aria-label={openInSingleMetricViewerAriaLabelText} - /> - - ); - }, - }); - } - - columns.push({ - align: RIGHT_ALIGNMENT, - width: '60px', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.actionsColumnName', - defaultMessage: 'Actions', - }), - actions, - }); - - const getRowProps = item => { - return { - onMouseOver: () => this.onMouseOverRow(item), - onMouseLeave: () => this.onMouseLeaveRow(), - }; + columns.push({ + align: RIGHT_ALIGNMENT, + width: '60px', + name: i18n.translate('xpack.ml.annotationsTable.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions, + }); + + const getRowProps = item => { + return { + onMouseOver: () => this.onMouseOverRow(item), + onMouseLeave: () => this.onMouseLeaveRow(), }; + }; - return ( - - - - ); - } + return ( + + + + ); } -); - -export { AnnotationsTable }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 1d1b785600f97..11e196b1c8e3f 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -6,18 +6,12 @@ import jobConfig from '../../../../../common/types/__mocks__/job_config_farequote'; import mockAnnotations from './__mocks__/mock_annotations.json'; -import './annotations_table.test.mocks'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { AnnotationsTable } from './annotations_table'; -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - addBasePath: () => {}, -})); - jest.mock('../../../services/job_service', () => ({ mlJobService: { getJob: jest.fn(), @@ -38,19 +32,17 @@ jest.mock('../../../services/ml_api_service', () => { describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Initialization with job config prop.', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Initialization with annotations prop.', () => { - const wrapper = shallowWithIntl( - - ); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/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 deleted file mode 100644 index 4a29fec03da85..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts +++ /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 { chromeServiceMock } from '../../../../../../../../../src/core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index 074a584f3a136..c16dc37097b13 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -11,10 +11,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public'; import { checkPermission } from '../../privilege/check_privilege'; @@ -29,465 +29,452 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getIndexPatternIdFromName } from '../../util/index_utils'; import { replaceStringTokens } from '../../util/string_utils'; - /* * Component for rendering the links menu inside a cell in the anomalies table. */ -export const LinksMenu = injectI18n( - class LinksMenu extends Component { - static propTypes = { - anomaly: PropTypes.object.isRequired, - bounds: PropTypes.object.isRequired, - showViewSeriesLink: PropTypes.bool, - isAggregatedData: PropTypes.bool, - interval: PropTypes.string, - showRuleEditorFlyout: PropTypes.func, +class LinksMenuUI extends Component { + static propTypes = { + anomaly: PropTypes.object.isRequired, + bounds: PropTypes.object.isRequired, + showViewSeriesLink: PropTypes.bool, + isAggregatedData: PropTypes.bool, + interval: PropTypes.string, + showRuleEditorFlyout: PropTypes.func, + }; + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + toasts: [], }; + } - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - toasts: [], - }; - } - - openCustomUrl = customUrl => { - const { anomaly, interval, isAggregatedData, intl } = this.props; - - console.log('Anomalies Table - open customUrl for record:', anomaly); - - // If url_value contains $earliest$ and $latest$ tokens, add in times to the source record. - // Create a copy of the record as we are adding properties into it. - const record = _.cloneDeep(anomaly.source); - const timestamp = record.timestamp; - const configuredUrlValue = customUrl.url_value; - const timeRangeInterval = parseInterval(customUrl.time_range); - if (configuredUrlValue.includes('$earliest$')) { - let earliestMoment = moment(timestamp); - if (timeRangeInterval !== null) { - earliestMoment.subtract(timeRangeInterval); - } else { - earliestMoment = moment(timestamp).startOf(interval); - if (interval === 'hour') { - // Start from the previous hour. - earliestMoment.subtract(1, 'h'); - } + openCustomUrl = customUrl => { + const { anomaly, interval, isAggregatedData } = this.props; + + console.log('Anomalies Table - open customUrl for record:', anomaly); + + // If url_value contains $earliest$ and $latest$ tokens, add in times to the source record. + // Create a copy of the record as we are adding properties into it. + const record = _.cloneDeep(anomaly.source); + const timestamp = record.timestamp; + const configuredUrlValue = customUrl.url_value; + const timeRangeInterval = parseInterval(customUrl.time_range); + if (configuredUrlValue.includes('$earliest$')) { + let earliestMoment = moment(timestamp); + if (timeRangeInterval !== null) { + earliestMoment.subtract(timeRangeInterval); + } else { + earliestMoment = moment(timestamp).startOf(interval); + if (interval === 'hour') { + // Start from the previous hour. + earliestMoment.subtract(1, 'h'); } - record.earliest = earliestMoment.toISOString(); // e.g. 2016-02-08T16:00:00.000Z } + record.earliest = earliestMoment.toISOString(); // e.g. 2016-02-08T16:00:00.000Z + } - if (configuredUrlValue.includes('$latest$')) { - let latestMoment = moment(timestamp).add(record.bucket_span, 's'); - if (timeRangeInterval !== null) { - latestMoment.add(timeRangeInterval); - } else { - if (isAggregatedData === true) { - latestMoment = moment(timestamp).endOf(interval); - if (interval === 'hour') { - // Show to the end of the next hour. - latestMoment.add(1, 'h'); // e.g. 2016-02-08T18:59:59.999Z - } + if (configuredUrlValue.includes('$latest$')) { + let latestMoment = moment(timestamp).add(record.bucket_span, 's'); + if (timeRangeInterval !== null) { + latestMoment.add(timeRangeInterval); + } else { + if (isAggregatedData === true) { + latestMoment = moment(timestamp).endOf(interval); + if (interval === 'hour') { + // Show to the end of the next hour. + latestMoment.add(1, 'h'); // e.g. 2016-02-08T18:59:59.999Z } } - record.latest = latestMoment.toISOString(); } + record.latest = latestMoment.toISOString(); + } - // If url_value contains $mlcategoryterms$ or $mlcategoryregex$, add in the - // terms and regex for the selected categoryId to the source record. - if ( - (configuredUrlValue.includes('$mlcategoryterms$') || - configuredUrlValue.includes('$mlcategoryregex$')) && - _.has(record, 'mlcategory') - ) { - const jobId = record.job_id; - - // mlcategory in the source record will be an array - // - use first value (will only ever be more than one if influenced by category other than by/partition/over). - const categoryId = record.mlcategory[0]; - - ml.results - .getCategoryDefinition(jobId, categoryId) - .then(resp => { - // Prefix each of the terms with '+' so that the Elasticsearch Query String query - // run in a drilldown Kibana dashboard has to match on all terms. - const termsArray = resp.terms.split(' ').map(term => `+${term}`); - record.mlcategoryterms = termsArray.join(' '); - record.mlcategoryregex = resp.regex; - - // Replace any tokens in the configured url_value with values from the source record, - // and then open link in a new tab/window. - const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); - }) - .catch(resp => { - console.log('openCustomUrl(): error loading categoryDefinition:', resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', - defaultMessage: - 'Unable to open link as an error occurred loading details on category ID {categoryId}', - }, - { - categoryId, - } - ) - ); - }); - } else { - // Replace any tokens in the configured url_value with values from the source record, - // and then open link in a new tab/window. - const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); - } - }; + // If url_value contains $mlcategoryterms$ or $mlcategoryregex$, add in the + // terms and regex for the selected categoryId to the source record. + if ( + (configuredUrlValue.includes('$mlcategoryterms$') || + configuredUrlValue.includes('$mlcategoryregex$')) && + _.has(record, 'mlcategory') + ) { + const jobId = record.job_id; + + // mlcategory in the source record will be an array + // - use first value (will only ever be more than one if influenced by category other than by/partition/over). + const categoryId = record.mlcategory[0]; + + ml.results + .getCategoryDefinition(jobId, categoryId) + .then(resp => { + // Prefix each of the terms with '+' so that the Elasticsearch Query String query + // run in a drilldown Kibana dashboard has to match on all terms. + const termsArray = resp.terms.split(' ').map(term => `+${term}`); + record.mlcategoryterms = termsArray.join(' '); + record.mlcategoryregex = resp.regex; + + // Replace any tokens in the configured url_value with values from the source record, + // and then open link in a new tab/window. + const urlPath = replaceStringTokens(customUrl.url_value, record, true); + openCustomUrlWindow(urlPath, customUrl); + }) + .catch(resp => { + console.log('openCustomUrl(): error loading categoryDefinition:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', { + defaultMessage: + 'Unable to open link as an error occurred loading details on category ID {categoryId}', + values: { + categoryId, + }, + }) + ); + }); + } else { + // Replace any tokens in the configured url_value with values from the source record, + // and then open link in a new tab/window. + const urlPath = getUrlForRecord(customUrl, record); + openCustomUrlWindow(urlPath, customUrl); + } + }; - viewSeries = () => { - const record = this.props.anomaly.source; - const bounds = this.props.bounds; - const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z - const to = bounds.max.toISOString(); + viewSeries = () => { + const record = this.props.anomaly.source; + const bounds = this.props.bounds; + const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z + const to = bounds.max.toISOString(); - // Zoom to show 50 buckets either side of the record. - const recordTime = moment(record.timestamp); - const zoomFrom = recordTime.subtract(50 * record.bucket_span, 's').toISOString(); - const zoomTo = recordTime.add(100 * record.bucket_span, 's').toISOString(); + // Zoom to show 50 buckets either side of the record. + const recordTime = moment(record.timestamp); + const zoomFrom = recordTime.subtract(50 * record.bucket_span, 's').toISOString(); + const zoomTo = recordTime.add(100 * record.bucket_span, 's').toISOString(); - // Extract the by, over and partition fields for the record. - const entityCondition = {}; + // Extract the by, over and partition fields for the record. + const entityCondition = {}; - if (_.has(record, 'partition_field_value')) { - entityCondition[record.partition_field_name] = record.partition_field_value; - } + if (_.has(record, 'partition_field_value')) { + entityCondition[record.partition_field_name] = record.partition_field_value; + } - if (_.has(record, 'over_field_value')) { - entityCondition[record.over_field_name] = record.over_field_value; - } + if (_.has(record, 'over_field_value')) { + entityCondition[record.over_field_name] = record.over_field_value; + } - if (_.has(record, 'by_field_value')) { - // Note that analyses with by and over fields, will have a top-level by_field_name, - // but the by_field_value(s) will be in the nested causes array. - // TODO - drilldown from cause in expanded row only? - entityCondition[record.by_field_name] = record.by_field_value; - } + if (_.has(record, 'by_field_value')) { + // Note that analyses with by and over fields, will have a top-level by_field_name, + // but the by_field_value(s) will be in the nested causes array. + // TODO - drilldown from cause in expanded row only? + entityCondition[record.by_field_name] = record.by_field_value; + } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { - jobIds: [record.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + // Use rison to build the URL . + const _g = rison.encode({ + ml: { + jobIds: [record.job_id], + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + time: { + from: from, + to: to, + mode: 'absolute', + }, + }); + + const _a = rison.encode({ + mlTimeSeriesExplorer: { + zoom: { + from: zoomFrom, + to: zoomTo, }, - time: { - from: from, - to: to, - mode: 'absolute', + detectorIndex: record.detector_index, + entities: entityCondition, + }, + query: { + query_string: { + analyze_wildcard: true, + query: '*', }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { - zoom: { - from: zoomFrom, - to: zoomTo, - }, - detectorIndex: record.detector_index, - entities: entityCondition, - }, - query: { - query_string: { - analyze_wildcard: true, - query: '*', + }, + }); + + // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. + let path = '#/timeseriesexplorer'; + path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; + window.open(path, '_blank'); + }; + + viewExamples = () => { + const categoryId = this.props.anomaly.entityValue; + const record = this.props.anomaly.source; + + const job = mlJobService.getJob(this.props.anomaly.jobId); + if (job === undefined) { + console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', { + defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}', + values: { + jobId: this.props.anomaly.jobId, }, - }, - }); - - // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = '#/timeseriesexplorer'; - path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; - window.open(path, '_blank'); - }; - - viewExamples = () => { - const { intl } = this.props; - const categoryId = this.props.anomaly.entityValue; - const record = this.props.anomaly.source; - - const job = mlJobService.getJob(this.props.anomaly.jobId); - if (job === undefined) { - console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', - defaultMessage: - 'Unable to view examples as no details could be found for job ID {jobId}', - }, - { - jobId: this.props.anomaly.jobId, - } - ) - ); - return; - } - const categorizationFieldName = job.analysis_config.categorization_field_name; - const datafeedIndices = job.datafeed_config.indices; - // Find the type of the categorization field i.e. text (preferred) or keyword. - // Uses the first matching field found in the list of indices in the datafeed_config. - // attempt to load the field type using each index. we have to do it this way as _field_caps - // doesn't specify which index a field came from unless there is a clash. - let i = 0; - findFieldType(datafeedIndices[i]); - - function findFieldType(index) { - getFieldTypeFromMapping(index, categorizationFieldName) - .then(resp => { - if (resp !== '') { - createAndOpenUrl(index, resp); + }) + ); + return; + } + const categorizationFieldName = job.analysis_config.categorization_field_name; + const datafeedIndices = job.datafeed_config.indices; + // Find the type of the categorization field i.e. text (preferred) or keyword. + // Uses the first matching field found in the list of indices in the datafeed_config. + // attempt to load the field type using each index. we have to do it this way as _field_caps + // doesn't specify which index a field came from unless there is a clash. + let i = 0; + findFieldType(datafeedIndices[i]); + + function findFieldType(index) { + getFieldTypeFromMapping(index, categorizationFieldName) + .then(resp => { + if (resp !== '') { + createAndOpenUrl(index, resp); + } else { + i++; + if (i < datafeedIndices.length) { + findFieldType(datafeedIndices[i]); } else { - i++; - if (i < datafeedIndices.length) { - findFieldType(datafeedIndices[i]); - } else { - error(); - } + error(); } - }) - .catch(() => { - error(); - }); - } + } + }) + .catch(() => { + error(); + }); + } - function createAndOpenUrl(index, categorizationFieldType) { - // Find the ID of the index pattern with a title attribute which matches the - // index configured in the datafeed. If a Kibana index pattern has not been created - // for this index, then the user will see a warning message on the Discover tab advising - // them that no matching index pattern has been configured. - const indexPatternId = getIndexPatternIdFromName(index) || index; - - // Get the definition of the category and use the terms or regex to view the - // matching events in the Kibana Discover tab depending on whether the - // categorization field is of mapping type text (preferred) or keyword. - ml.results - .getCategoryDefinition(record.job_id, categoryId) - .then(resp => { - let query = null; - // Build query using categorization regex (if keyword type) or terms (if text type). - // Check for terms or regex in case categoryId represents an anomaly from the absence of the - // categorization field in documents (usually indicated by a categoryId of -1). - if (categorizationFieldType === ES_FIELD_TYPES.KEYWORD) { - if (resp.regex) { - query = { - language: SEARCH_QUERY_LANGUAGE.LUCENE, - query: `${categorizationFieldName}:/${resp.regex}/`, - }; - } - } else { - if (resp.terms) { - const escapedTerms = escapeDoubleQuotes(resp.terms); - query = { - language: SEARCH_QUERY_LANGUAGE.KUERY, - query: - `${categorizationFieldName}:"` + - escapedTerms.split(' ').join(`" and ${categorizationFieldName}:"`) + - '"', - }; - } + function createAndOpenUrl(index, categorizationFieldType) { + // Find the ID of the index pattern with a title attribute which matches the + // index configured in the datafeed. If a Kibana index pattern has not been created + // for this index, then the user will see a warning message on the Discover tab advising + // them that no matching index pattern has been configured. + const indexPatternId = getIndexPatternIdFromName(index) || index; + + // Get the definition of the category and use the terms or regex to view the + // matching events in the Kibana Discover tab depending on whether the + // categorization field is of mapping type text (preferred) or keyword. + ml.results + .getCategoryDefinition(record.job_id, categoryId) + .then(resp => { + let query = null; + // Build query using categorization regex (if keyword type) or terms (if text type). + // Check for terms or regex in case categoryId represents an anomaly from the absence of the + // categorization field in documents (usually indicated by a categoryId of -1). + if (categorizationFieldType === ES_FIELD_TYPES.KEYWORD) { + if (resp.regex) { + query = { + language: SEARCH_QUERY_LANGUAGE.LUCENE, + query: `${categorizationFieldName}:/${resp.regex}/`, + }; } - - const recordTime = moment(record.timestamp); - const from = recordTime.toISOString(); - const to = recordTime.add(record.bucket_span, 's').toISOString(); - - // Use rison to build the URL . - const _g = rison.encode({ - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const appStateProps = { - index: indexPatternId, - filters: [], - }; - if (query !== null) { - appStateProps.query = query; + } else { + if (resp.terms) { + const escapedTerms = escapeDoubleQuotes(resp.terms); + query = { + language: SEARCH_QUERY_LANGUAGE.KUERY, + query: + `${categorizationFieldName}:"` + + escapedTerms.split(' ').join(`" and ${categorizationFieldName}:"`) + + '"', + }; } - const _a = rison.encode(appStateProps); - - // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. - let path = chrome.getBasePath(); - path += '/app/kibana#/discover'; - path += '?_g=' + _g; - path += '&_a=' + encodeURIComponent(_a); - window.open(path, '_blank'); - }) - .catch(resp => { - console.log('viewExamples(): error loading categoryDefinition:', resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', - defaultMessage: - 'Unable to view examples as an error occurred loading details on category ID {categoryId}', - }, - { - categoryId, - } - ) - ); - }); - } - - function error() { - console.log( - `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, - datafeedIndices - ); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', - defaultMessage: - 'Unable to view examples of documents with mlcategory {categoryId} ' + - 'as no mapping could be found for the categorization field {categorizationFieldName}', - }, - { - categoryId, - categorizationFieldName, - } - ) - ); - } - }; + } - onButtonClick = () => { - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; + const recordTime = moment(record.timestamp); + const from = recordTime.toISOString(); + const to = recordTime.add(record.bucket_span, 's').toISOString(); - closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; - - render() { - const { anomaly, showViewSeriesLink, intl } = this.props; - const canConfigureRules = isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'); - - const button = ( - - ); + time: { + from: from, + to: to, + mode: 'absolute', + }, + }); - const items = []; - if (anomaly.customUrls !== undefined) { - anomaly.customUrls.forEach((customUrl, index) => { - items.push( - { - this.closePopover(); - this.openCustomUrl(customUrl); - }} - > - {customUrl.url_name} - + const appStateProps = { + index: indexPatternId, + filters: [], + }; + if (query !== null) { + appStateProps.query = query; + } + const _a = rison.encode(appStateProps); + + // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. + const { basePath } = this.props.kibana.services.http; + let path = basePath.get(); + path += '/app/kibana#/discover'; + path += '?_g=' + _g; + path += '&_a=' + encodeURIComponent(_a); + window.open(path, '_blank'); + }) + .catch(resp => { + console.log('viewExamples(): error loading categoryDefinition:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', { + defaultMessage: + 'Unable to view examples as an error occurred loading details on category ID {categoryId}', + values: { + categoryId, + }, + }) ); }); - } - - if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { - items.push( - { - this.closePopover(); - this.viewSeries(); - }} - > - - - ); - } + } - if (anomaly.entityName === 'mlcategory') { + function error() { + console.log( + `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, + datafeedIndices + ); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', { + defaultMessage: + 'Unable to view examples of documents with mlcategory {categoryId} ' + + 'as no mapping could be found for the categorization field {categorizationFieldName}', + values: { + categoryId, + categorizationFieldName, + }, + }) + ); + } + }; + + onButtonClick = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + render() { + const { anomaly, showViewSeriesLink } = this.props; + const canConfigureRules = isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'); + + const button = ( + + ); + + const items = []; + if (anomaly.customUrls !== undefined) { + anomaly.customUrls.forEach((customUrl, index) => { items.push( { this.closePopover(); - this.viewExamples(); + this.openCustomUrl(customUrl); }} > - + {customUrl.url_name} ); - } + }); + } - if (canConfigureRules) { - items.push( - { - this.closePopover(); - this.props.showRuleEditorFlyout(anomaly); - }} - > - - - ); - } + if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { + items.push( + { + this.closePopover(); + this.viewSeries(); + }} + > + + + ); + } - return ( - { + this.closePopover(); + this.viewExamples(); + }} > - - + + ); } + + if (canConfigureRules) { + items.push( + { + this.closePopover(); + this.props.showRuleEditorFlyout(anomaly); + }} + > + + + ); + } + + return ( + + + + ); } -); +} + +export const LinksMenu = withKibana(LinksMenuUI); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts index f047ae800266b..7b113326a1f97 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts @@ -6,8 +6,6 @@ import { influencerColorScaleFactory } from './use_color_range'; -jest.mock('../../contexts/ui/use_ui_chrome_context'); - describe('useColorRange', () => { test('influencerColorScaleFactory(1)', () => { const influencerColorScale = influencerColorScaleFactory(1); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index f9c5e6ff81f9e..83f143b75b388 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -11,7 +11,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import { i18n } from '@kbn/i18n'; -import { useUiChromeContext } from '../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../contexts/kibana/use_ui_settings_context'; /** * Custom color scale factory that takes the amount of feature influencers @@ -150,11 +150,7 @@ export const useColorRange = ( colorRangeScale = COLOR_RANGE_SCALE.LINEAR, featureCount = 1 ) => { - const euiTheme = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode') - ? euiThemeDark - : euiThemeLight; + const euiTheme = useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight; const colorRanges = { [COLOR_RANGE.BLUE]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)], diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 1b06b72d1387c..056fd04857cba 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -7,10 +7,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; -jest.mock('ui/i18n', () => ({ - I18nContext: jest.fn(), -})); - import { FieldTitleBar } from './field_title_bar'; // helper to let PropTypes throw errors instead of just doing console.error() diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4460ced7079c3..d0fde87bf1c2a 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -7,10 +7,9 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'src/plugins/data/public'; +import { Query, IndexPattern } from 'src/plugins/data/public'; import { EuiButton } from '@elastic/eui'; import { setFullTimeRange } from './full_time_range_selector_service'; -import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; interface Props { indexPattern: IndexPattern; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index e69aaf2ede037..265e11ce6a154 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -7,10 +7,9 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -import { timefilter } from 'ui/timefilter'; import { Query } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -24,6 +23,7 @@ export async function setFullTimeRange( query: Query ): Promise { try { + const timefilter = getTimefilter(); const resp = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, @@ -35,6 +35,7 @@ export async function setFullTimeRange( }); return resp; } catch (resp) { + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.fullTimeRangeSelector.errorSettingTimeRangeNotification', { defaultMessage: 'An error occurred setting the time range.', @@ -45,20 +46,12 @@ export async function setFullTimeRange( } export function getTimeFilterRange(): TimeRange { - let from = 0; - let to = 0; - const fromString = timefilter.getTime().from; - const toString = timefilter.getTime().to; - if (typeof fromString === 'string' && typeof toString === 'string') { - const fromMoment = dateMath.parse(fromString); - const toMoment = dateMath.parse(toString); - if (typeof fromMoment !== 'undefined' && typeof toMoment !== 'undefined') { - const fromMs = fromMoment.valueOf(); - const toMs = toMoment.valueOf(); - from = fromMs; - to = toMs; - } - } + const timefilter = getTimefilter(); + const fromMoment = dateMath.parse(timefilter.getTime().from); + const toMoment = dateMath.parse(timefilter.getTime().to); + const from = fromMoment !== undefined ? fromMoment.valueOf() : 0; + const to = toMoment !== undefined ? toMoment.valueOf() : 0; + return { to, from, diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx index f1d9dcb0ec795..bd2ec2d1511a3 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -22,8 +22,7 @@ import { import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - +import { useMlKibana } from '../../contexts/kibana'; import { Dictionary } from '../../../../common/types/common'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { ml } from '../../services/ml_api_service'; @@ -114,6 +113,9 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); const flyoutEl = useRef<{ flyout: HTMLElement }>(null); + const { + services: { notifications }, + } = useMlKibana(); // Ensure JobSelectionBar gets updated when selection via globalState changes. useEffect(() => { @@ -178,7 +180,8 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J }) .catch((err: any) => { console.error('Error fetching jobs with time range', err); // eslint-disable-line - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', }), diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 563156ea98055..214bb90917302 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -7,9 +7,9 @@ import { difference } from 'lodash'; import { useEffect } from 'react'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { useUrlState } from '../../util/url_state'; @@ -27,6 +27,7 @@ function getInvalidJobIds(jobs: MlJobWithTimeRange[], ids: string[]) { function warnAboutInvalidJobIds(invalidIds: string[]) { if (invalidIds.length > 0) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning( i18n.translate('xpack.ml.jobSelect.requestedJobsDoesNotExistWarningMessage', { defaultMessage: `Requested @@ -66,6 +67,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[], dateFormatTz: string useEffect(() => { // if there are no valid ids, warn and then select the first job if (validIds.length === 0 && jobs.length > 0) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning( i18n.translate('xpack.ml.jobSelect.noJobsSelectedWarningMessage', { defaultMessage: 'No jobs selected, auto selecting first job', diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js index e604c101a9994..0f3c6d25fe641 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js @@ -10,15 +10,16 @@ import { uniqueId } from 'lodash'; import { FilterBar } from './filter_bar'; import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; import { getSuggestions, getKqlQueryValues } from './utils'; +import { getDocLinks } from '../../util/dependency_cache'; function getErrorWithLink(errorMessage) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); return ( {`${errorMessage} Input must be valid `} {'Kibana Query Language'} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4e74a4bd545a3..610d924651406 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -8,8 +8,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KqlFilterBar } from './kql_filter_bar'; -jest.mock('ui/new_platform'); - const defaultProps = { indexPattern: { title: '.ml-anomalies-*', @@ -33,6 +31,12 @@ const defaultProps = { placeholder: undefined, }; +jest.mock('../../util/dependency_cache', () => ({ + getAutocomplete: () => ({ + getQuerySuggestions: () => {}, + }), +})); + describe('KqlFilterBar', () => { test('snapshot', () => { const wrapper = shallow(); 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 index bb7b143c948d8..bb3e676f4b410 100644 --- 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 @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { npStart } from 'ui/new_platform'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; +import { getAutocomplete } from '../../util/dependency_cache'; export function getSuggestions(query, selectionStart, indexPattern, boolFilter) { - return npStart.plugins.data.autocomplete.getQuerySuggestions({ + const autocomplete = getAutocomplete(); + return autocomplete.getQuerySuggestions({ language: 'kuery', indexPatterns: [indexPattern], boolFilter, diff --git a/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js index 6d5f4e267abcf..d79fe14cbac4e 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js +++ b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MLRequestFailure } from '../../util/ml_error'; import { i18n } from '@kbn/i18n'; @@ -18,6 +18,7 @@ function errorNotify(text, resp) { err = new Error(text); } + const toastNotifications = getToastNotifications(); toastNotifications.addError(new MLRequestFailure(err, resp), { title: i18n.translate('xpack.ml.messagebarService.errorTitle', { defaultMessage: 'An error has ocurred', diff --git a/x-pack/legacy/plugins/ml/public/application/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 index e9bec02868b71..b03281bf30399 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,15 +10,36 @@ import { MemoryRouter } from 'react-router-dom'; import { EuiSuperDatePicker } from '@elastic/eui'; -import { uiTimefilterMock } from '../../../contexts/ui/__mocks__/mocks_jest'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { TopNav } from './top_nav'; -uiTimefilterMock.enableAutoRefreshSelector(); -uiTimefilterMock.enableTimeRangeSelector(); - -jest.mock('../../../contexts/ui/use_ui_context'); +jest.mock('../../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + uiSettings: { get: jest.fn() }, + data: { + query: { + timefilter: { + timefilter: { + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + }, + }; + }, +})); const noop = () => {}; @@ -41,7 +62,6 @@ describe('Navigation Menu: ', () => { ); expect(wrapper.find(TopNav)).toHaveLength(1); - expect(wrapper.find('EuiSuperDatePicker')).toHaveLength(1); expect(refreshListener).toBeCalledTimes(0); refreshSubscription.unsubscribe(); diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx index a63a07b3ec538..edc6aece265f3 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx @@ -7,15 +7,14 @@ import React, { FC, Fragment, useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui'; -import { TimeHistory } from 'ui/timefilter'; -import { TimeRange } from 'src/plugins/data/public'; +import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public'; import { mlTimefilterRefresh$, mlTimefilterTimeChange$, } from '../../../services/timefilter_refresh_service'; -import { useUiContext } from '../../../contexts/ui/use_ui_context'; import { useUrlState } from '../../../util/url_state'; +import { useMlKibana } from '../../../contexts/kibana'; interface Duration { start: string; @@ -27,7 +26,7 @@ interface RefreshInterval { value: number; } -function getRecentlyUsedRangesFactory(timeHistory: TimeHistory) { +function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) { return function(): Duration[] { return ( timeHistory.get()?.map(({ from, to }: TimeRange) => { @@ -45,9 +44,12 @@ function updateLastRefresh(timeRange: OnRefreshProps) { } export const TopNav: FC = () => { - const { chrome, timefilter, timeHistory } = useUiContext(); + const { services } = useMlKibana(); + const config = services.uiSettings; + const { timefilter, history } = services.data.query.timefilter; + const [globalState, setGlobalState] = useUrlState('_g'); - const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(timeHistory); + const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history); const [refreshInterval, setRefreshInterval] = useState( globalState?.refreshInterval ?? timefilter.getRefreshInterval() @@ -66,7 +68,7 @@ export const TopNav: FC = () => { timefilter.isTimeRangeSelectorEnabled() ); - const dateFormat = chrome.getUiSettingsClient().get('dateFormat'); + const dateFormat = config.get('dateFormat'); useEffect(() => { const subscriptions = new Subscription(); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index da9a3c7437bf4..5d8c644d6d0eb 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -40,7 +40,7 @@ exports[`ConditionsSectionExpression renders when enabled with no conditions sup exports[`ConditionsSectionExpression renders when enabled with one condition 1`] = ` - - - { - this.setState({ - isAppliesToOpen: true, - isOperatorValueOpen: false, - }); - }; - - closeAppliesTo = () => { - this.setState({ - isAppliesToOpen: false, - }); - }; - - openOperatorValue = () => { - this.setState({ - isAppliesToOpen: false, - isOperatorValueOpen: true, - }); - }; - - closeOperatorValue = () => { - this.setState({ - isOperatorValueOpen: false, - }); - }; - - changeAppliesTo = event => { - const { index, operator, value, updateCondition } = this.props; - updateCondition(index, event.target.value, operator, value); - }; - - changeOperator = event => { - const { index, appliesTo, value, updateCondition } = this.props; - updateCondition(index, appliesTo, event.target.value, value); - }; - - changeValue = event => { - const { index, appliesTo, operator, updateCondition } = this.props; - updateCondition(index, appliesTo, operator, +event.target.value); +export class ConditionExpression extends Component { + static propTypes = { + index: PropTypes.number.isRequired, + appliesTo: PropTypes.oneOf([ + APPLIES_TO.ACTUAL, + APPLIES_TO.TYPICAL, + APPLIES_TO.DIFF_FROM_TYPICAL, + ]), + operator: PropTypes.oneOf([ + OPERATOR.LESS_THAN, + OPERATOR.LESS_THAN_OR_EQUAL, + OPERATOR.GREATER_THAN, + OPERATOR.GREATER_THAN_OR_EQUAL, + ]), + value: PropTypes.number.isRequired, + updateCondition: PropTypes.func.isRequired, + deleteCondition: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + isAppliesToOpen: false, + isOperatorValueOpen: false, }; + } - renderAppliesToPopover() { - return ( -
- - - -
- -
+ openAppliesTo = () => { + this.setState({ + isAppliesToOpen: true, + isOperatorValueOpen: false, + }); + }; + + closeAppliesTo = () => { + this.setState({ + isAppliesToOpen: false, + }); + }; + + openOperatorValue = () => { + this.setState({ + isAppliesToOpen: false, + isOperatorValueOpen: true, + }); + }; + + closeOperatorValue = () => { + this.setState({ + isOperatorValueOpen: false, + }); + }; + + changeAppliesTo = event => { + const { index, operator, value, updateCondition } = this.props; + updateCondition(index, event.target.value, operator, value); + }; + + changeOperator = event => { + const { index, appliesTo, value, updateCondition } = this.props; + updateCondition(index, appliesTo, event.target.value, value); + }; + + changeValue = event => { + const { index, appliesTo, operator, updateCondition } = this.props; + updateCondition(index, appliesTo, operator, +event.target.value); + }; + + renderAppliesToPopover() { + return ( +
+ + + +
+
- ); - } - - renderOperatorValuePopover() { - return ( -
- - - -
- - - - - - - - - -
+
+ ); + } + + renderOperatorValuePopover() { + return ( +
+ + + +
+ + + + + + + + +
- ); - } - - render() { - const { index, appliesTo, operator, value, deleteCondition } = this.props; - - return ( - - - - } - value={appliesToText(appliesTo)} - isActive={this.state.isAppliesToOpen} - onClick={this.openAppliesTo} - /> - } - isOpen={this.state.isAppliesToOpen} - closePopover={this.closeAppliesTo} - panelPaddingSize="none" - ownFocus - withTitle - anchorPosition="downLeft" - > - {this.renderAppliesToPopover()} - - - - - - } - value={`${value}`} - isActive={this.state.isOperatorValueOpen} - onClick={this.openOperatorValue} - /> - } - isOpen={this.state.isOperatorValueOpen} - closePopover={this.closeOperatorValue} - panelPaddingSize="none" - ownFocus - withTitle - anchorPosition="downLeft" - > - {this.renderOperatorValuePopover()} - - - - deleteCondition(index)} - iconType="trash" - aria-label={this.props.intl.formatMessage({ - id: 'xpack.ml.ruleEditor.conditionExpression.deleteConditionButtonAriaLabel', +
+ ); + } + + render() { + const { index, appliesTo, operator, value, deleteCondition } = this.props; + + return ( + + + + } + value={appliesToText(appliesTo)} + isActive={this.state.isAppliesToOpen} + onClick={this.openAppliesTo} + /> + } + isOpen={this.state.isAppliesToOpen} + closePopover={this.closeAppliesTo} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderAppliesToPopover()} + + + + + + } + value={`${value}`} + isActive={this.state.isOperatorValueOpen} + onClick={this.openOperatorValue} + /> + } + isOpen={this.state.isOperatorValueOpen} + closePopover={this.closeOperatorValue} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderOperatorValuePopover()} + + + + deleteCondition(index)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.ml.ruleEditor.conditionExpression.deleteConditionButtonAriaLabel', + { defaultMessage: 'Delete condition', - })} - /> - - - ); - } + } + )} + /> + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js index eaab9c2ad7a62..79ed620d151f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js @@ -29,7 +29,7 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -42,7 +42,7 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 1f66cf95553b9..6dabf78b31002 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -28,8 +28,6 @@ import { EuiTitle, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; import { checkPermission } from '../../privilege/check_privilege'; @@ -50,682 +48,679 @@ import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS, } from '../../../../common/constants/detector_rule'; import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; -import { metadata } from 'ui/metadata'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +class RuleEditorFlyoutUI extends Component { + static propTypes = { + setShowFunction: PropTypes.func.isRequired, + unsetShowFunction: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + anomaly: {}, + job: {}, + ruleIndex: -1, + rule: getNewRuleDefaults(), + skipModelUpdate: false, + isConditionsEnabled: false, + isScopeEnabled: false, + filterListIds: [], + isFlyoutVisible: false, + }; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-rules.html`; + this.partitioningFieldNames = []; + this.canGetFilters = checkPermission('canGetFilters'); + } -export const RuleEditorFlyout = injectI18n( - class RuleEditorFlyout extends Component { - static propTypes = { - setShowFunction: PropTypes.func.isRequired, - unsetShowFunction: PropTypes.func.isRequired, - }; + componentDidMount() { + if (typeof this.props.setShowFunction === 'function') { + this.props.setShowFunction(this.showFlyout); + } + } - constructor(props) { - super(props); - - this.state = { - anomaly: {}, - job: {}, - ruleIndex: -1, - rule: getNewRuleDefaults(), - skipModelUpdate: false, - isConditionsEnabled: false, - isScopeEnabled: false, - filterListIds: [], + componentWillUnmount() { + if (typeof this.props.unsetShowFunction === 'function') { + this.props.unsetShowFunction(); + } + } + + showFlyout = anomaly => { + let ruleIndex = -1; + const job = mlJobService.getJob(anomaly.jobId); + if (job === undefined) { + // No details found for this job, display an error and + // don't open the Flyout as no edits can be made without the job. + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.unableToConfigureRulesNotificationMesssage', + { + defaultMessage: + 'Unable to configure rules as an error occurred obtaining details for job ID {jobId}', + values: { jobId: anomaly.jobId }, + } + ) + ); + this.setState({ + job, isFlyoutVisible: false, - }; + }); - this.partitioningFieldNames = []; - this.canGetFilters = checkPermission('canGetFilters'); + return; } - componentDidMount() { - if (typeof this.props.setShowFunction === 'function') { - this.props.setShowFunction(this.showFlyout); - } + this.partitioningFieldNames = getPartitioningFieldNames(job, anomaly.detectorIndex); + + // Check if any rules are configured for this detector. + const detectorIndex = anomaly.detectorIndex; + const detector = job.analysis_config.detectors[detectorIndex]; + if (detector.custom_rules === undefined) { + ruleIndex = 0; } - componentWillUnmount() { - if (typeof this.props.unsetShowFunction === 'function') { - this.props.unsetShowFunction(); - } + let isConditionsEnabled = false; + if (ruleIndex === 0) { + // Configuring the first rule for a detector. + isConditionsEnabled = this.partitioningFieldNames.length === 0; } - showFlyout = anomaly => { - let ruleIndex = -1; - const { intl } = this.props; - const job = mlJobService.getJob(anomaly.jobId); - if (job === undefined) { - // No details found for this job, display an error and - // don't open the Flyout as no edits can be made without the job. - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.ruleEditor.ruleEditorFlyout.unableToConfigureRulesNotificationMesssage', - defaultMessage: - 'Unable to configure rules as an error occurred obtaining details for job ID {jobId}', - }, - { jobId: anomaly.jobId } - ) - ); - this.setState({ - job, - isFlyoutVisible: false, + this.setState({ + anomaly, + job, + ruleIndex, + isConditionsEnabled, + isScopeEnabled: false, + isFlyoutVisible: true, + }); + + if (this.partitioningFieldNames.length > 0 && this.canGetFilters) { + // Load the current list of filters. These are used for configuring rule scope. + ml.filters + .filters() + .then(filters => { + const filterListIds = filters.map(filter => filter.filter_id); + this.setState({ + filterListIds, + }); + }) + .catch(resp => { + console.log('Error loading list of filters:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithLoadingFilterListsNotificationMesssage', + { + defaultMessage: 'Error loading the filter lists used in the rule scope', + } + ) + ); }); + } + }; + + closeFlyout = () => { + this.setState({ isFlyoutVisible: false }); + }; + + setEditRuleIndex = ruleIndex => { + const detectorIndex = this.state.anomaly.detectorIndex; + const detector = this.state.job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + const rule = + rules === undefined || ruleIndex >= rules.length ? getNewRuleDefaults() : rules[ruleIndex]; + + const isConditionsEnabled = + this.partitioningFieldNames.length === 0 || + (rule.conditions !== undefined && rule.conditions.length > 0); + const isScopeEnabled = rule.scope !== undefined && Object.keys(rule.scope).length > 0; + if (isScopeEnabled === true) { + // Add 'enabled:true' to mark them as selected in the UI. + Object.keys(rule.scope).forEach(field => { + rule.scope[field].enabled = true; + }); + } - return; + this.setState({ + ruleIndex, + rule, + isConditionsEnabled, + isScopeEnabled, + }); + }; + + onSkipResultChange = e => { + const checked = e.target.checked; + this.setState(prevState => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_RESULT); + if (idx === -1 && checked) { + actions.push(ACTION.SKIP_RESULT); + } else if (idx > -1 && !checked) { + actions.splice(idx, 1); } - this.partitioningFieldNames = getPartitioningFieldNames(job, anomaly.detectorIndex); - - // Check if any rules are configured for this detector. - const detectorIndex = anomaly.detectorIndex; - const detector = job.analysis_config.detectors[detectorIndex]; - if (detector.custom_rules === undefined) { - ruleIndex = 0; + return { + rule: { ...prevState.rule, actions }, + }; + }); + }; + + onSkipModelUpdateChange = e => { + const checked = e.target.checked; + this.setState(prevState => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_MODEL_UPDATE); + if (idx === -1 && checked) { + actions.push(ACTION.SKIP_MODEL_UPDATE); + } else if (idx > -1 && !checked) { + actions.splice(idx, 1); } - let isConditionsEnabled = false; - if (ruleIndex === 0) { - // Configuring the first rule for a detector. - isConditionsEnabled = this.partitioningFieldNames.length === 0; + return { + rule: { ...prevState.rule, actions }, + }; + }); + }; + + onConditionsEnabledChange = e => { + const isConditionsEnabled = e.target.checked; + this.setState(prevState => { + let conditions; + if (isConditionsEnabled === false) { + // Clear any conditions that have been added. + conditions = []; + } else { + // Add a default new condition. + conditions = [getNewConditionDefaults()]; } - this.setState({ - anomaly, - job, - ruleIndex, + return { + rule: { ...prevState.rule, conditions }, isConditionsEnabled, - isScopeEnabled: false, - isFlyoutVisible: true, - }); + }; + }); + }; - if (this.partitioningFieldNames.length > 0 && this.canGetFilters) { - // Load the current list of filters. These are used for configuring rule scope. - ml.filters - .filters() - .then(filters => { - const filterListIds = filters.map(filter => filter.filter_id); - this.setState({ - filterListIds, - }); - }) - .catch(resp => { - console.log('Error loading list of filters:', resp); - toastNotifications.addDanger( - intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithLoadingFilterListsNotificationMesssage', - defaultMessage: 'Error loading the filter lists used in the rule scope', - }) - ); - }); + addCondition = () => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + conditions.push(getNewConditionDefaults()); + + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + updateCondition = (index, appliesTo, operator, value) => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions[index] = { + applies_to: appliesTo, + operator, + value, + }; } - }; - closeFlyout = () => { - this.setState({ isFlyoutVisible: false }); - }; + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + deleteCondition = index => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions.splice(index, 1); + } - setEditRuleIndex = ruleIndex => { - const detectorIndex = this.state.anomaly.detectorIndex; - const detector = this.state.job.analysis_config.detectors[detectorIndex]; - const rules = detector.custom_rules; - const rule = - rules === undefined || ruleIndex >= rules.length ? getNewRuleDefaults() : rules[ruleIndex]; - - const isConditionsEnabled = - this.partitioningFieldNames.length === 0 || - (rule.conditions !== undefined && rule.conditions.length > 0); - const isScopeEnabled = rule.scope !== undefined && Object.keys(rule.scope).length > 0; - if (isScopeEnabled === true) { - // Add 'enabled:true' to mark them as selected in the UI. - Object.keys(rule.scope).forEach(field => { - rule.scope[field].enabled = true; - }); + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + onScopeEnabledChange = e => { + const isScopeEnabled = e.target.checked; + this.setState(prevState => { + const rule = { ...prevState.rule }; + if (isScopeEnabled === false) { + // Clear scope property. + delete rule.scope; } - this.setState({ - ruleIndex, + return { rule, - isConditionsEnabled, isScopeEnabled, - }); - }; - - onSkipResultChange = e => { - const checked = e.target.checked; - this.setState(prevState => { - const actions = [...prevState.rule.actions]; - const idx = actions.indexOf(ACTION.SKIP_RESULT); - if (idx === -1 && checked) { - actions.push(ACTION.SKIP_RESULT); - } else if (idx > -1 && !checked) { - actions.splice(idx, 1); - } - - return { - rule: { ...prevState.rule, actions }, - }; - }); - }; - - onSkipModelUpdateChange = e => { - const checked = e.target.checked; - this.setState(prevState => { - const actions = [...prevState.rule.actions]; - const idx = actions.indexOf(ACTION.SKIP_MODEL_UPDATE); - if (idx === -1 && checked) { - actions.push(ACTION.SKIP_MODEL_UPDATE); - } else if (idx > -1 && !checked) { - actions.splice(idx, 1); - } - - return { - rule: { ...prevState.rule, actions }, - }; - }); - }; - - onConditionsEnabledChange = e => { - const isConditionsEnabled = e.target.checked; - this.setState(prevState => { - let conditions; - if (isConditionsEnabled === false) { - // Clear any conditions that have been added. - conditions = []; - } else { - // Add a default new condition. - conditions = [getNewConditionDefaults()]; - } - - return { - rule: { ...prevState.rule, conditions }, - isConditionsEnabled, - }; - }); - }; - - addCondition = () => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - conditions.push(getNewConditionDefaults()); - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - updateCondition = (index, appliesTo, operator, value) => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - if (index < conditions.length) { - conditions[index] = { - applies_to: appliesTo, - operator, - value, - }; - } - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - deleteCondition = index => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - if (index < conditions.length) { - conditions.splice(index, 1); - } - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - onScopeEnabledChange = e => { - const isScopeEnabled = e.target.checked; - this.setState(prevState => { - const rule = { ...prevState.rule }; - if (isScopeEnabled === false) { - // Clear scope property. - delete rule.scope; - } - - return { - rule, - isScopeEnabled, - }; - }); - }; - - updateScope = (fieldName, filterId, filterType, enabled) => { - this.setState(prevState => { - let scope = { ...prevState.rule.scope }; - if (scope === undefined) { - scope = {}; - } + }; + }); + }; + + updateScope = (fieldName, filterId, filterType, enabled) => { + this.setState(prevState => { + let scope = { ...prevState.rule.scope }; + if (scope === undefined) { + scope = {}; + } - scope[fieldName] = { - filter_id: filterId, - filter_type: filterType, - enabled, - }; + scope[fieldName] = { + filter_id: filterId, + filter_type: filterType, + enabled, + }; - return { - rule: { ...prevState.rule, scope }, - }; - }); - }; + return { + rule: { ...prevState.rule, scope }, + }; + }); + }; - saveEdit = () => { - const { rule, ruleIndex } = this.state; + saveEdit = () => { + const { rule, ruleIndex } = this.state; - this.updateRuleAtIndex(ruleIndex, rule); - }; + this.updateRuleAtIndex(ruleIndex, rule); + }; - updateRuleAtIndex = (ruleIndex, editedRule) => { - const { intl } = this.props; - const { job, anomaly } = this.state; + updateRuleAtIndex = (ruleIndex, editedRule) => { + const { toasts } = this.props.kibana.services.notifications; + const { job, anomaly } = this.state; - const jobId = job.job_id; - const detectorIndex = anomaly.detectorIndex; + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; - saveJobRule(job, detectorIndex, ruleIndex, editedRule) - .then(resp => { - if (resp.success) { - toastNotifications.add({ - title: intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageTitle', - defaultMessage: 'Changes to {jobId} detector rules saved', - }, - { jobId } - ), - color: 'success', - iconType: 'check', - text: intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageDescription', + saveJobRule(job, detectorIndex, ruleIndex, editedRule) + .then(resp => { + if (resp.success) { + toasts.add({ + title: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageTitle', + { + defaultMessage: 'Changes to {jobId} detector rules saved', + values: { jobId }, + } + ), + color: 'success', + iconType: 'check', + text: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageDescription', + { defaultMessage: 'Note that changes will take effect for new results only.', - }), - }); - this.closeFlyout(); - } else { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', - defaultMessage: 'Error saving changes to {jobId} detector rules', - }, - { jobId } - ) - ); - } - }) - .catch(error => { - console.error(error); - toastNotifications.addDanger( - intl.formatMessage( + } + ), + }); + this.closeFlyout(); + } else { + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', defaultMessage: 'Error saving changes to {jobId} detector rules', - }, - { jobId } + values: { jobId }, + } ) ); - }); - }; - - deleteRuleAtIndex = index => { - const { intl } = this.props; - const { job, anomaly } = this.state; - const jobId = job.job_id; - const detectorIndex = anomaly.detectorIndex; - - deleteJobRule(job, detectorIndex, index) - .then(resp => { - if (resp.success) { - toastNotifications.addSuccess( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.ruleDeletedFromJobDetectorNotificationMessage', - defaultMessage: 'Rule deleted from {jobId} detector', - }, - { jobId } - ) - ); - this.closeFlyout(); - } else { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', - defaultMessage: 'Error deleting rule from {jobId} detector', - }, - { jobId } - ) - ); - } - }) - .catch(error => { - console.error(error); - let errorMessage = intl.formatMessage( + } + }) + .catch(error => { + console.error(error); + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', - defaultMessage: 'Error deleting rule from {jobId} detector', - }, - { jobId } + defaultMessage: 'Error saving changes to {jobId} detector rules', + values: { jobId }, + } + ) + ); + }); + }; + + deleteRuleAtIndex = index => { + const { toasts } = this.props.kibana.services.notifications; + const { job, anomaly } = this.state; + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; + + deleteJobRule(job, detectorIndex, index) + .then(resp => { + if (resp.success) { + toasts.addSuccess( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.ruleDeletedFromJobDetectorNotificationMessage', + { + defaultMessage: 'Rule deleted from {jobId} detector', + values: { jobId }, + } + ) ); - if (error.message) { - errorMessage += ` : ${error.message}`; - } - toastNotifications.addDanger(errorMessage); - }); - }; - - addItemToFilterList = (item, filterId, closeFlyoutOnAdd) => { - const { intl } = this.props; - addItemToFilter(item, filterId) - .then(() => { - if (closeFlyoutOnAdd === true) { - toastNotifications.add({ - title: intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageTitle', - defaultMessage: 'Added {item} to {filterId}', - }, - { item, filterId } - ), - color: 'success', - iconType: 'check', - text: intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageDescription', - defaultMessage: 'Note that changes will take effect for new results only.', - }), - }); - this.closeFlyout(); - } - }) - .catch(error => { - console.log(`Error adding ${item} to filter ${filterId}:`, error); - toastNotifications.addDanger( - intl.formatMessage( + this.closeFlyout(); + } else { + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithAddingItemToFilterListNotificationMessage', - defaultMessage: 'An error occurred adding {item} to filter {filterId}', - }, - { item, filterId } + defaultMessage: 'Error deleting rule from {jobId} detector', + values: { jobId }, + } ) ); - }); - }; - - render() { - const { intl } = this.props; - const { - isFlyoutVisible, - job, - anomaly, - ruleIndex, - rule, - filterListIds, - isConditionsEnabled, - isScopeEnabled, - } = this.state; - - if (isFlyoutVisible === false) { - return null; - } + } + }) + .catch(error => { + console.error(error); + let errorMessage = i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', + { + defaultMessage: 'Error deleting rule from {jobId} detector', + values: { jobId }, + } + ); + if (error.message) { + errorMessage += ` : ${error.message}`; + } + toasts.addDanger(errorMessage); + }); + }; + + addItemToFilterList = (item, filterId, closeFlyoutOnAdd) => { + const { toasts } = this.props.kibana.services.notifications; + addItemToFilter(item, filterId) + .then(() => { + if (closeFlyoutOnAdd === true) { + toasts.add({ + title: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageTitle', + { + defaultMessage: 'Added {item} to {filterId}', + values: { item, filterId }, + } + ), + color: 'success', + iconType: 'check', + text: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageDescription', + { + defaultMessage: 'Note that changes will take effect for new results only.', + } + ), + }); + this.closeFlyout(); + } + }) + .catch(error => { + console.log(`Error adding ${item} to filter ${filterId}:`, error); + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithAddingItemToFilterListNotificationMessage', + { + defaultMessage: 'An error occurred adding {item} to filter {filterId}', + values: { item, filterId }, + } + ) + ); + }); + }; + + render() { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = this.props.kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`; + const { + isFlyoutVisible, + job, + anomaly, + ruleIndex, + rule, + filterListIds, + isConditionsEnabled, + isScopeEnabled, + } = this.state; + + if (isFlyoutVisible === false) { + return null; + } - let flyout; - - if (ruleIndex === -1) { - flyout = ( - - - -

+ let flyout; + + if (ruleIndex === -1) { + flyout = ( + + + +

+ +

+
+
+ + + + + + + + + -

-
-
- - - - - - - - - - - - - - -
- ); - } else { - const detectorIndex = anomaly.detectorIndex; - const detector = job.analysis_config.detectors[detectorIndex]; - const rules = detector.custom_rules; - const isCreate = rules === undefined || ruleIndex >= rules.length; - - const hasPartitioningFields = - this.partitioningFieldNames && this.partitioningFieldNames.length > 0; - const conditionSupported = - CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(anomaly.source.function) === -1; - const conditionsText = intl.formatMessage({ - id: 'xpack.ml.ruleEditor.ruleEditorFlyout.conditionsDescription', + + + + + + ); + } else { + const detectorIndex = anomaly.detectorIndex; + const detector = job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + const isCreate = rules === undefined || ruleIndex >= rules.length; + + const hasPartitioningFields = + this.partitioningFieldNames && this.partitioningFieldNames.length > 0; + const conditionSupported = + CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(anomaly.source.function) === -1; + const conditionsText = i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.conditionsDescription', + { defaultMessage: 'Add numeric conditions for when the rule applies. Multiple conditions are combined using AND.', - }); - - flyout = ( - - - -

- {isCreate === true ? ( - - ) : ( - - )} -

-
-
- - - - - -

+ } + ); + + flyout = ( + + + +

+ {isCreate === true ? ( - - - ), - }} + id="xpack.ml.ruleEditor.ruleEditorFlyout.createRuleTitle" + defaultMessage="Create rule" /> -

- - - - - -

+ ) : ( -

-
- + )} +

+ + + + + + + +

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

+
- + - -

- -

-
- - {conditionSupported === true ? ( - +

+ - ) : ( - - } - iconType="iInCircle" +

+ + + + + + +

+ - )} - - - - - - + + + {conditionSupported === true ? ( + - + ) : ( } - color="warning" - iconType="help" - > -

+ iconType="iInCircle" + /> + )} + + + + + + + + + } + color="warning" + iconType="help" + > +

+ +

+

+ +

+
+ + + + + + -

-

+ + + + -

- - - - - - - - - - - - - - - - - - - ); - } - - return {flyout}; + +
+
+
+ + ); } + + return {flyout}; } -); +} + +export const RuleEditorFlyout = withKibana(RuleEditorFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index c498a75fa2ec1..7259e4f7d5016 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -49,6 +49,12 @@ jest.mock('../../privilege/check_privilege', () => ({ checkPermission: () => true, })); +jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -77,9 +83,17 @@ function prepareTest() { const requiredProps = { setShowFunction, unsetShowFunction, + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; - const component = ; + const component = ; const wrapper = shallowWithIntl(component); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index d82f78cbc4e1a..b512f6d7c014c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,7 +17,7 @@ exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` />, }, Object { - "description": { - const enteredValue = event.target.value; - this.setState({ - value: enteredValue !== '' ? +enteredValue : '', - }); - }; + this.state = { value }; + } + + onChangeValue = event => { + const enteredValue = event.target.value; + this.setState({ + value: enteredValue !== '' ? +enteredValue : '', + }); + }; - onUpdateClick = () => { - const { conditionIndex, updateConditionValue } = this.props; - updateConditionValue(conditionIndex, this.state.value); - }; + onUpdateClick = () => { + const { conditionIndex, updateConditionValue } = this.props; + updateConditionValue(conditionIndex, this.state.value); + }; - render() { - const { intl } = this.props; - const value = this.state.value; - return ( - + render() { + const value = this.state.value; + return ( + + + + + + + + + + {value !== '' && ( - + this.onUpdateClick()}> - + - - - - {value !== '' && ( - - this.onUpdateClick()}> - - - - )} - - ); - } + )} + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index b9027c932e302..5d8916cf22a12 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -31,7 +31,7 @@ function prepareTest(updateConditionValueFn, appliesTo) { updateConditionValue: updateConditionValueFn, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); return wrapper; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js index a5ed7c3753b2f..98e027ec4f365 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -28,9 +28,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; -import { metadata } from 'ui/metadata'; -// metadata.branch corresponds to the version used in documentation links. -const jobTipsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/create-jobs.html#job-tips`; +import { getDocLinks } from '../../util/dependency_cache'; // don't use something like plugins/ml/../common // because it won't work with the jest tests @@ -253,6 +251,8 @@ export class ValidateJob extends Component { }; render() { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); + const jobTipsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#job-tips`; // only set to false if really false and not another falsy value, so it defaults to true. const fill = this.props.fill === false ? false : true; // default to false if not explicitly set to true diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index 575320f728627..cc8a5abb4e9ab 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -9,6 +9,13 @@ import React from 'react'; import { ValidateJob } from './validate_job_view'; +jest.mock('../../util/dependency_cache', () => ({ + getDocLinks: () => ({ + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }), +})); + const job = { job_id: 'test-id', }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts index 629e52797fb42..7ebbd45fd372a 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts @@ -4,12 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - KibanaContext, - KibanaContextValue, - SavedSearchQuery, - KibanaConfigTypeFix, -} from './kibana_context'; -export { useKibanaContext } from './use_kibana_context'; -export { useCurrentIndexPattern } from './use_current_index_pattern'; -export { useCurrentSavedSearch } from './use_current_saved_search'; +export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useUiSettings } from './use_ui_settings_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 9d0a3bc43e258..aaf539322809b 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -4,43 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { CoreStart } from 'kibana/public'; import { - IndexPattern, - IndexPatternsContract, -} from '../../../../../../../../src/plugins/data/public'; -import { SavedSearchSavedObject } from '../../../../common/types/kibana'; - -// set() method is missing in original d.ts -export interface KibanaConfigTypeFix extends KibanaConfig { - set(key: string, value: any): void; -} + useKibana, + KibanaReactContextValue, +} from '../../../../../../../../src/plugins/kibana_react/public'; -export interface KibanaContextValue { - combinedQuery: any; - currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null - currentSavedSearch: SavedSearchSavedObject | null; - indexPatterns: IndexPatternsContract; - kibanaConfig: KibanaConfigTypeFix; +interface StartPlugins { + data: DataPublicPluginStart; } - -export type SavedSearchQuery = object; - -// This context provides dependencies which can be injected -// via angularjs only (like services, currentIndexPattern etc.). -// Because we cannot just import these dependencies, the default value -// for the context is just {} and of type `Partial` -// for the angularjs based dependencies. Therefore, the -// actual dependencies are set like we did previously with KibanaContext -// in the wrapping angularjs directive. In the custom hook we check if -// the dependencies are present with error reporting if they weren't -// added properly. That's why in tests, these custom hooks must not -// be mocked, instead ` needs -// to be used. This guarantees that we have both properly set up -// TypeScript support and runtime checks for these dependencies. -// Multiple custom hooks can be created to access subsets of -// the overall context value if necessary too, -// see useCurrentIndexPattern() for example. -export const KibanaContext = React.createContext>({}); +export type StartServices = CoreStart & StartPlugins; +// eslint-disable-next-line react-hooks/rules-of-hooks +export const useMlKibana = () => useKibana(); +export type MlKibanaReactContextValue = KibanaReactContextValue; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts similarity index 64% rename from x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts index 4964d727a0452..92f59f62f8a25 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiChromeMock } from './mocks_jest'; +import { useMlKibana } from './kibana_context'; -export const useUiChromeContext = () => uiChromeMock; +export const useUiSettings = () => { + return useMlKibana().services.uiSettings; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_patterns.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_patterns.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_config.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_config.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__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/ml/__mocks__/saved_search.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts similarity index 66% rename from x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts index 46178a7d02977..7b48d717ea190 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/timefilter', () => { - return {}; -}); +export { MlContext, MlContextValue, SavedSearchQuery } from './ml_context'; +export { useMlContext } from './use_ml_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.ts new file mode 100644 index 0000000000000..6b6c34dd37968 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.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 React from 'react'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../../src/plugins/data/public'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; + +export interface MlContextValue { + combinedQuery: any; + currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null + currentSavedSearch: SavedSearchSavedObject | null; + indexPatterns: IndexPatternsContract; + kibanaConfig: any; // IUiSettingsClient; +} + +export type SavedSearchQuery = object; + +// This context provides dependencies which can be injected +// via angularjs only (like services, currentIndexPattern etc.). +// Because we cannot just import these dependencies, the default value +// for the context is just {} and of type `Partial` +// for the angularjs based dependencies. Therefore, the +// actual dependencies are set like we did previously with KibanaContext +// in the wrapping angularjs directive. In the custom hook we check if +// the dependencies are present with error reporting if they weren't +// added properly. That's why in tests, these custom hooks must not +// be mocked, instead ` needs +// to be used. This guarantees that we have both properly set up +// TypeScript support and runtime checks for these dependencies. +// Multiple custom hooks can be created to access subsets of +// the overall context value if necessary too, +// see useCurrentIndexPattern() for example. +export const MlContext = React.createContext>({}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts similarity index 83% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts index 62be409882dff..4469deae4d15e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext } from './kibana_context'; +import { MlContext } from './ml_context'; export const useCurrentIndexPattern = () => { - const context = useContext(KibanaContext); + const context = useContext(MlContext); if (context.currentIndexPattern === undefined) { throw new Error('currentIndexPattern is undefined'); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts similarity index 83% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts index 1147b905f237e..d31d9dd5bead9 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext } from './kibana_context'; +import { MlContext } from './ml_context'; export const useCurrentSavedSearch = () => { - const context = useContext(KibanaContext); + const context = useContext(MlContext); if (context.currentSavedSearch === undefined) { throw new Error('currentSavedSearch is undefined'); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts similarity index 74% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts index 658a6980aa1ae..c8bf54309bd9e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext, KibanaContextValue } from './kibana_context'; +import { MlContext, MlContextValue } from './ml_context'; -export const useKibanaContext = () => { - const context = useContext(KibanaContext); +export const useMlContext = () => { + const context = useContext(MlContext); if ( context.combinedQuery === undefined || @@ -21,5 +21,5 @@ export const useKibanaContext = () => { throw new Error('required attribute is undefined'); } - return context as KibanaContextValue; + return context as MlContextValue; }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts deleted file mode 100644 index 785daec0ab369..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts +++ /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. - */ - -export const uiChromeMock = { - getBasePath: () => 'basePath', - getUiSettingsClient: () => { - return { - get: (key: string) => { - switch (key) { - case 'dateFormat': - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'theme:darkMode': - return false; - case 'timepicker:timeDefaults': - return {}; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - }, - }; - }, -}; - -interface RefreshInterval { - value: number; - pause: boolean; -} - -const time = { - from: 'Thu Aug 29 2019 02:04:19 GMT+0200', - to: 'Sun Sep 29 2019 01:45:36 GMT+0200', -}; - -export const uiTimefilterMock = { - isAutoRefreshSelectorEnabled() { - return this._isAutoRefreshSelectorEnabled; - }, - isTimeRangeSelectorEnabled() { - return this._isTimeRangeSelectorEnabled; - }, - enableAutoRefreshSelector() { - this._isAutoRefreshSelectorEnabled = true; - }, - enableTimeRangeSelector() { - this._isTimeRangeSelectorEnabled = true; - }, - getEnabledUpdated$() { - return { subscribe: jest.fn() }; - }, - getRefreshInterval() { - return this.refreshInterval; - }, - getRefreshIntervalUpdate$() { - return { subscribe: jest.fn() }; - }, - getTime: () => time, - getTimeUpdate$() { - return { subscribe: jest.fn() }; - }, - _isAutoRefreshSelectorEnabled: false, - _isTimeRangeSelectorEnabled: false, - refreshInterval: { value: 0, pause: true }, - on: (event: string, reload: () => void) => {}, - setRefreshInterval(refreshInterval: RefreshInterval) { - this.refreshInterval = refreshInterval; - }, -}; - -export const uiTimeHistoryMock = { - get: () => [time], -}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts deleted file mode 100644 index cd3d80bed8d14..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts +++ /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 { Subject } from 'rxjs'; - -export const uiChromeMock = { - getBasePath: () => 'basePath', - getUiSettingsClient: () => { - return { - get: (key: string) => { - switch (key) { - case 'dateFormat': - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'theme:darkMode': - return false; - case 'timepicker:timeDefaults': - return {}; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - }, - }; - }, -}; - -interface RefreshInterval { - value: number; - pause: boolean; -} - -const time = { - from: 'Thu Aug 29 2019 02:04:19 GMT+0200', - to: 'Sun Sep 29 2019 01:45:36 GMT+0200', -}; - -export const uiTimefilterMock = { - isAutoRefreshSelectorEnabled() { - return this._isAutoRefreshSelectorEnabled; - }, - isTimeRangeSelectorEnabled() { - return this._isTimeRangeSelectorEnabled; - }, - enableAutoRefreshSelector() { - this._isAutoRefreshSelectorEnabled = true; - }, - enableTimeRangeSelector() { - this._isTimeRangeSelectorEnabled = true; - }, - getActiveBounds() { - return; - }, - getEnabledUpdated$() { - return { subscribe: () => {} }; - }, - getFetch$() { - return new Subject(); - }, - getRefreshInterval() { - return this.refreshInterval; - }, - getRefreshIntervalUpdate$() { - return { subscribe: () => {} }; - }, - getTime: () => time, - getTimeUpdate$() { - return { subscribe: () => {} }; - }, - _isAutoRefreshSelectorEnabled: false, - _isTimeRangeSelectorEnabled: false, - refreshInterval: { value: 0, pause: true }, - on: (event: string, reload: () => void) => {}, - setRefreshInterval(refreshInterval: RefreshInterval) { - this.refreshInterval = refreshInterval; - }, -}; - -export const uiTimeHistoryMock = { - get: () => [time], -}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts deleted file mode 100644 index 0aaaa868c490a..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.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 { uiChromeMock, uiTimefilterMock, uiTimeHistoryMock } from './mocks_jest'; - -export const useUiContext = () => ({ - chrome: uiChromeMock, - timefilter: uiTimefilterMock, - timeHistory: uiTimeHistoryMock, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts deleted file mode 100644 index 18cbb49181e38..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts +++ /dev/null @@ -1,9 +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. - */ - -// We only export UiContext but not any custom hooks, because if we'd import them -// from here, mocking the hook from jest tests won't work as expected. -export { UiContext } from './ui_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx b/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx deleted file mode 100644 index 4cb97cf5639fe..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx +++ /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 React from 'react'; - -import chrome from 'ui/chrome'; -import { timefilter, timeHistory } from 'ui/timefilter'; - -// This provides ui/* based imports via React Context. -// Because these dependencies can use regular imports, -// they are just passed on as the default value -// of the Context which means it's not necessary -// to add ... to the -// wrapping angular directive, reducing a lot of boilerplate. -// The custom hooks like useUiContext() need to be mocked in -// tests because we rely on the properly set up default value. -// Different custom hooks can be created to access parts only -// from the full context value, see useUiChromeContext() as an example. -export const UiContext = React.createContext({ - chrome, - timefilter, - timeHistory, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts deleted file mode 100644 index 1765bdb23df7f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.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 { useContext } from 'react'; - -import { UiContext } from './ui_context'; - -export const useUiChromeContext = () => { - return useContext(UiContext).chrome; -}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts index 924e1228c27ab..9182487cedb51 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts @@ -5,7 +5,6 @@ */ import { getAnalysisType, isOutlierAnalysis } from './analytics'; -jest.mock('ui/new_platform'); describe('Data Frame Analytics: Analytics utils', () => { test('getAnalysisType()', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 12d441a9a23ec..f87578c4bce48 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -12,7 +12,7 @@ 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'; +import { SavedSearchQuery } from '../../contexts/ml'; import { SortDirection } from '../../components/ml_in_memory_table'; export type IndexName = string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 95e1b15d548c1..df2ca3e7de657 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -17,7 +17,7 @@ import { LoadingPanel } from '../loading_panel'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; interface GetDataFrameAnalyticsResponse { count: number; @@ -64,7 +64,7 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { undefined ); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -107,7 +107,7 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { try { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 1e24bfec6de5e..23dd1ae288d8e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -19,7 +19,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ErrorCallout } from '../error_callout'; import { getDependentVar, @@ -50,6 +50,9 @@ interface Props { } export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const { + services: { docLinks }, + } = useMlKibana(); const [isLoading, setIsLoading] = useState(false); const [confusionMatrixData, setConfusionMatrixData] = useState([]); const [columns, setColumns] = useState([]); @@ -217,6 +220,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) return ; } + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + return ( = ({ jobConfig, jobStatus, searchQuery }) iconType="help" iconSide="left" color="primary" - href={`https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics-evaluate.html#ml-dfanalytics-classification`} + href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-classification`} > {i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.classificationDocsLink', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx index 85794cf813ab5..849a0793a094b 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -39,7 +39,7 @@ import { import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 013ea8ddc78a5..ca8fd68079f7e 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -7,11 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; - -jest.mock('../../../../../contexts/ui/use_ui_chrome_context'); -jest.mock('ui/new_platform'); +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { Exploration } from './exploration'; @@ -24,9 +21,9 @@ jest.mock('react', () => { describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const wrapper = shallow( - + - + ); // Without the jobConfig being loaded, the component will just return empty. expect(wrapper.text()).toMatch(''); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index bd1b60d92403e..ce72e90b4c230 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -64,11 +64,11 @@ import { Query as QueryType, } from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; const FEATURE_INFLUENCE = 'feature_influence'; @@ -115,13 +115,13 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [searchError, setSearchError] = useState(undefined); const [searchString, setSearchString] = useState(undefined); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const initializeJobCapsService = async () => { if (jobConfig !== undefined) { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } 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 index fe2676053dde3..74937bf761285 100644 --- 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 @@ -16,7 +16,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ErrorCallout } from '../error_callout'; import { getValuesFromResponse, @@ -46,6 +46,10 @@ interface Props { const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const [trainingEval, setTrainingEval] = useState(defaultEval); const [generalizationEval, setGeneralizationEval] = useState(defaultEval); const [isLoadingTraining, setIsLoadingTraining] = useState(false); @@ -256,7 +260,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) iconType="help" iconSide="left" color="primary" - href={`https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} + href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 7399828bcd642..569cf21792874 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,7 +17,7 @@ import { LoadingPanel } from '../loading_panel'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; interface GetDataFrameAnalyticsResponse { count: number; @@ -64,7 +64,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { undefined ); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -98,7 +98,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { try { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } 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 index 971fa99f2e93f..118652318785d 100644 --- 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 @@ -39,7 +39,7 @@ import { import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 2a939d93a48b3..08cc54ec39c6f 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -18,7 +18,6 @@ jest.mock('../../../../../privilege/check_privilege', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); -jest.mock('ui/new_platform'); describe('DeleteAction', () => { test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 30f87ad8a375b..19a3857f3f71c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,7 +5,6 @@ */ import StatsMock from './__mocks__/analytics_stats.json'; -jest.mock('ui/new_platform'); import { isCompletedAnalyticsJob, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4ccfa8a562c6c..0e32bdb39e690 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -6,7 +6,7 @@ import React, { useEffect } from 'react'; -import { timefilter } from 'ui/timefilter'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { DEFAULT_REFRESH_INTERVAL_MS, @@ -18,6 +18,9 @@ import { useRefreshAnalyticsList } from '../../../../common'; export const useRefreshInterval = ( setBlockRefresh: React.Dispatch> ) => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; + const { refresh } = useRefreshAnalyticsList(); useEffect(() => { let analyticsRefreshInterval: null | number = null; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index abb35e50ec2a2..7d58f0df12e6c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsButton } from './create_analytics_button'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index d5d509826667c..cacb3744f7ab4 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsFlyout } from './create_analytics_flyout'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index d01bae9616708..af6dadf236932 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsForm } from './create_analytics_form'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); @@ -29,14 +29,27 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + docLinks: () => ({ + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }), + }, + }; + }, +})); + describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const { getLastHookValue } = getMountedHook(); const props = getLastHookValue(); const wrapper = mount( - + - + ); const euiFormRows = wrapper.find('EuiFormRow'); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index e68523733254e..338fa1e4ac328 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -21,11 +21,11 @@ import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; import { Field } from '../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { JOB_TYPES, @@ -45,8 +45,12 @@ import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../comm import { shouldAddAsDepVarOption, OMIT_FIELDS } from './form_options_validation'; export const CreateAnalyticsForm: FC = ({ actions, state }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; const { @@ -92,7 +96,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // that an analytics jobs is not able to identify outliers if there are no numeric fields present. const validateSourceIndexFields = async () => { try { - const indexPattern: IndexPattern = await kibanaContext.indexPatterns.get( + const indexPattern: IndexPattern = await mlContext.indexPatterns.get( indexPatternsMap[sourceIndex].value ); const containsNumericalFields: boolean = indexPattern.fields.some( @@ -207,7 +211,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta sourceIndexContainsNumericalFields: true, }); try { - const indexPattern: IndexPattern = await kibanaContext.indexPatterns.get( + const indexPattern: IndexPattern = await mlContext.indexPatterns.get( indexPatternsMap[sourceIndex].value ); @@ -456,7 +460,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta )}
{i18n.translate( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 3298a7d00253f..2bdcc28e31fff 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -7,8 +7,8 @@ import React from 'react'; import { mountHook } from 'test_utils/enzyme_helpers'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { getErrorMessage, useCreateAnalyticsForm } from './use_create_analytics_form'; @@ -16,7 +16,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index b2f9442f48edb..59474b63213a2 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { SimpleSavedObject } from 'src/core/public'; import { ml } from '../../../../../services/ml_api_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { useRefreshAnalyticsList, @@ -43,7 +43,7 @@ export function getErrorMessage(error: any) { } export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); const { refresh } = useRefreshAnalyticsList(); @@ -130,7 +130,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const indexPatternName = destinationIndex; try { - const newIndexPattern = await kibanaContext.indexPatterns.make(); + const newIndexPattern = await mlContext.indexPatterns.make(); Object.assign(newIndexPattern, { id: '', @@ -161,8 +161,8 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { // check if there's a default index pattern, if not, // set the newly created one as the default index pattern. - if (!kibanaContext.kibanaConfig.get('defaultIndex')) { - await kibanaContext.kibanaConfig.set('defaultIndex', id); + if (!mlContext.kibanaConfig.get('defaultIndex')) { + await mlContext.kibanaConfig.set('defaultIndex', id); } addRequestMessage({ @@ -226,7 +226,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { try { // Set the index pattern titles which the user can choose as the source. const indexPatternsMap: SourceIndexMap = {}; - const savedObjects = (await kibanaContext.indexPatterns.getCache()) || []; + const savedObjects = (await mlContext.indexPatterns.getCache()) || []; savedObjects.forEach((obj: SimpleSavedObject>) => { const title = obj?.attributes?.title; if (title !== undefined) { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index fb366b517f0b7..3c0c3fa0df87c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -16,6 +16,7 @@ import { } from '../../components/analytics_list/common'; export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { if (isDataFrameAnalyticsFailed(d.stats.state)) { await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true, true); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index da09c4842b843..6513cad808485 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -13,6 +13,7 @@ import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../.. import { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; export const startAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 84d1835c6e1e3..c92c03c3b0f16 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -16,6 +16,7 @@ import { } from '../../components/analytics_list/common'; export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { await ml.dataFrameAnalytics.stopDataFrameAnalytics( d.config.id, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 7c0bcac039164..ae0c034f972d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -22,8 +22,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { timefilter } from 'ui/timefilter'; import { isFullLicense } from '../license/check_license'; +import { useMlKibana } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; @@ -49,6 +49,8 @@ function startTrialDescription() { } export const DatavisualizerSelector: FC = () => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4fe4933261985..99cdc816dfe3d 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { @@ -22,7 +23,7 @@ import { import { WelcomeContent } from './welcome_content'; -export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, intl }) { +export function AboutPanel({ onFilePickerChange }) { return ( @@ -36,10 +37,12 @@ export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, i
onFilePickerChange(files)} className="file-datavisualizer-file-picker" /> @@ -51,7 +54,7 @@ export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, i ); -}); +} export function LoadingPanel() { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js index 40bf7a8ff5f21..516ac791fc677 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js @@ -7,7 +7,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; -import { metadata } from 'ui/metadata'; import { EuiComboBox, @@ -31,6 +30,7 @@ import { // getCharsetOptions, } from './options'; import { isTimestampFormatValid } from './overrides_validation'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { TIMESTAMP_OPTIONS, CUSTOM_DROPDOWN_OPTION } from './options/option_lists'; @@ -43,7 +43,7 @@ const quoteOptions = getQuoteOptions(); const LINES_TO_SAMPLE_VALUE_MIN = 3; const LINES_TO_SAMPLE_VALUE_MAX = 1000000; -export class Overrides extends Component { +class OverridesUI extends Component { constructor(props) { super(props); @@ -268,8 +268,8 @@ export class Overrides extends Component { const fieldOptions = getSortedFields(fields); const timestampFormatErrorsList = [this.customTimestampFormatErrors, timestampFormatError]; - // metadata.branch corresponds to the version used in documentation links. - const docsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern`; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = this.props.kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern`; const timestampFormatHelp = ( @@ -504,6 +504,8 @@ export class Overrides extends Component { } } +export const Overrides = withKibana(OverridesUI); + function selectedOption(opt) { return [{ label: opt || '' }]; } diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 9a66439adf697..ee0df7c9ab32e 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -9,6 +9,12 @@ import React from 'react'; import { Overrides } from './overrides'; +jest.mock('../../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + function getProps() { return { setOverrides: () => {}, @@ -17,6 +23,14 @@ function getProps() { defaultSettings: {}, setApplyOverrides: () => {}, fields: [], + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; } diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 324e64a674551..272ec2979ad2f 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiStepsHorizontal, EuiProgress, EuiSpacer } from '@elastic/eui'; @@ -15,7 +16,7 @@ export const IMPORT_STATUS = { FAILED: 'danger', }; -export const ImportProgress = injectI18n(function({ statuses, intl }) { +export function ImportProgress({ statuses }) { const { reading, readStatus, @@ -63,26 +64,36 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { completedStep = 5; } - let processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.processFileTitle', - defaultMessage: 'Process file', - }); - let createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIndexTitle', - defaultMessage: 'Create index', - }); - let createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIngestPipelineTitle', - defaultMessage: 'Create ingest pipeline', - }); - let uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.uploadDataTitle', - defaultMessage: 'Upload data', - }); - let createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIndexPatternTitle', - defaultMessage: 'Create index pattern', - }); + let processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.processFileTitle', + { + defaultMessage: 'Process file', + } + ); + let createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIndexTitle', + { + defaultMessage: 'Create index', + } + ); + let createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIngestPipelineTitle', + { + defaultMessage: 'Create ingest pipeline', + } + ); + let uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.uploadDataTitle', + { + defaultMessage: 'Upload data', + } + ); + let createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIndexPatternTitle', + { + defaultMessage: 'Create index pattern', + } + ); const creatingIndexStatus = (

@@ -103,10 +114,12 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { ); if (completedStep >= 0) { - processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.processingFileTitle', - defaultMessage: 'Processing file', - }); + processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.processingFileTitle', + { + defaultMessage: 'Processing file', + } + ); statusInfo = (

= 1) { - processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.fileProcessedTitle', - defaultMessage: 'File processed', - }); - createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexTitle', - defaultMessage: 'Creating index', - }); + processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.fileProcessedTitle', + { + defaultMessage: 'File processed', + } + ); + createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexTitle', + { + defaultMessage: 'Creating index', + } + ); statusInfo = createPipeline === true ? creatingIndexAndIngestPipelineStatus : creatingIndexStatus; } if (completedStep >= 2) { - createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.indexCreatedTitle', - defaultMessage: 'Index created', - }); - createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIngestPipelineTitle', - defaultMessage: 'Creating ingest pipeline', - }); + createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.indexCreatedTitle', + { + defaultMessage: 'Index created', + } + ); + createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIngestPipelineTitle', + { + defaultMessage: 'Creating ingest pipeline', + } + ); statusInfo = createPipeline === true ? creatingIndexAndIngestPipelineStatus : creatingIndexStatus; } if (completedStep >= 3) { - createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.ingestPipelineCreatedTitle', - defaultMessage: 'Ingest pipeline created', - }); - uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.uploadingDataTitle', - defaultMessage: 'Uploading data', - }); + createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.ingestPipelineCreatedTitle', + { + defaultMessage: 'Ingest pipeline created', + } + ); + uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.uploadingDataTitle', + { + defaultMessage: 'Uploading data', + } + ); statusInfo = ; } if (completedStep >= 4) { - uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.dataUploadedTitle', - defaultMessage: 'Data uploaded', - }); + uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.dataUploadedTitle', + { + defaultMessage: 'Data uploaded', + } + ); if (createIndexPattern === true) { - createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexPatternTitle', - defaultMessage: 'Creating index pattern', - }); + createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexPatternTitle', + { + defaultMessage: 'Creating index pattern', + } + ); statusInfo = (

= 5) { - createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.indexPatternCreatedTitle', - defaultMessage: 'Index pattern created', - }); + createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.indexPatternCreatedTitle', + { + defaultMessage: 'Index pattern created', + } + ); statusInfo = null; } @@ -240,7 +271,7 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { )} ); -}); +} function UploadFunctionProgress({ progress }) { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js index 2d431cc046462..94143ea354d70 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { @@ -19,7 +20,7 @@ import { import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor'; const EDITOR_HEIGHT = '300px'; -function AdvancedSettingsUi({ +export function AdvancedSettings({ index, indexPattern, initialized, @@ -35,7 +36,6 @@ function AdvancedSettingsUi({ onPipelineStringChange, indexNameError, indexPatternNameError, - intl, }) { return ( @@ -50,18 +50,22 @@ function AdvancedSettingsUi({ error={[indexNameError]} > @@ -131,8 +135,6 @@ function AdvancedSettingsUi({ ); } -export const AdvancedSettings = injectI18n(AdvancedSettingsUi); - function IndexSettings({ initialized, data, onChange }) { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4d066fa84f070..ba637c472333d 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; @@ -12,7 +12,7 @@ import { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; import { SimpleSettings } from './simple'; import { AdvancedSettings } from './advanced'; -export const ImportSettings = injectI18n(function({ +export const ImportSettings = ({ index, indexPattern, initialized, @@ -28,13 +28,11 @@ export const ImportSettings = injectI18n(function({ onPipelineStringChange, indexNameError, indexPatternNameError, - intl, -}) { +}) => { const tabs = [ { id: 'simple-settings', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importSettings.simpleTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.importSettings.simpleTabName', { defaultMessage: 'Simple', }), content: ( @@ -54,8 +52,7 @@ export const ImportSettings = injectI18n(function({ }, { id: 'advanced-settings', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importSettings.advancedTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.importSettings.advancedTabName', { defaultMessage: 'Advanced', }), content: ( @@ -88,4 +85,4 @@ export const ImportSettings = injectI18n(function({ {}} /> ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js index beee48d8cc577..8c6f569bf8605 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui'; -export const SimpleSettings = injectI18n(function({ +export const SimpleSettings = ({ index, initialized, onIndexChange, createIndexPattern, onCreateIndexPatternChange, indexNameError, - intl, -}) { +}) => { return ( @@ -62,4 +66,4 @@ export const SimpleSettings = injectI18n(function({ /> ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index f1cc456ae4de8..aaebca2f58963 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,15 +10,16 @@ import React, { Component } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import moment from 'moment'; -import uiChrome from 'ui/chrome'; + import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license/check_license'; import { checkPermission } from '../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; const RECHECK_DELAY_MS = 3000; -export class ResultsLinks extends Component { +class ResultsLinksUI extends Component { constructor(props) { super(props); @@ -76,6 +77,7 @@ export class ResultsLinks extends Component { ? `&_g=(time:(from:'${from}',mode:quick,to:'${to}'))` : ''; + const { basePath } = this.props.kibana.services.http; return ( {createIndexPattern && ( @@ -89,7 +91,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} + href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} /> )} @@ -139,7 +141,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} + href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} /> @@ -153,7 +155,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/management/kibana/index_patterns/${ + href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ createIndexPattern ? indexPatternId : '' }`} /> @@ -163,6 +165,8 @@ export class ResultsLinks extends Component { } } +export const ResultsLinks = withKibana(ResultsLinksUI); + async function getFullTimeRange(index, timeFieldName) { const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; const resp = await ml.getTimeFieldRange({ diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 6ff0eb86f2c55..df9d9c1f9a3bc 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + import React from 'react'; import { @@ -22,14 +24,11 @@ import { FileContents } from '../file_contents'; import { AnalysisSummary } from '../analysis_summary'; import { FieldsStats } from '../fields_stats'; -export const ResultsView = injectI18n(function({ data, fileName, results, showEditFlyout, intl }) { - console.log(results); - +export const ResultsView = ({ data, fileName, results, showEditFlyout }) => { const tabs = [ { id: 'file-stats', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', { defaultMessage: 'File stats', }), content: , @@ -78,4 +77,4 @@ export const ResultsView = injectI18n(function({ data, fileName, results, showEd ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 149e3d1818e64..9dcb9d25692e9 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -5,9 +5,9 @@ */ import React, { FC, Fragment } from 'react'; -import { timefilter } from 'ui/timefilter'; +import { IUiSettingsClient } from 'src/core/public'; -import { KibanaConfigTypeFix } from '../../contexts/kibana'; +import { useMlKibana } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { getIndexPatternsContract } from '../../util/index_utils'; @@ -15,10 +15,12 @@ import { getIndexPatternsContract } from '../../util/index_utils'; import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { - kibanaConfig: KibanaConfigTypeFix; + kibanaConfig: IUiSettingsClient; } export const FileDataVisualizerPage: FC = ({ kibanaConfig }) => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); const indexPatterns = getIndexPatternsContract(); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 9da1235a6becd..a2cc59bb38939 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -21,7 +21,7 @@ import { import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; export interface DocumentCountChartPoint { time: number | string; @@ -56,9 +56,7 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); - const IS_DARK_THEME = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode'); + const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; const EVENT_RATE_COLOR = themeName.euiColorVis2; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index a7ad315dd968f..cf0e3ec1a9c9b 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -23,7 +23,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format'; import { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service'; @@ -52,9 +52,7 @@ export const MetricDistributionChart: FC = ({ width, height, chartData, f defaultMessage: 'distribution', }); - const IS_DARK_THEME = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode'); + const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; const AREA_SERIES_COLOR = themeName.euiColorVis1; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 5036a7d44aa8c..01ece9beddcea 100644 --- a/x-pack/legacy/plugins/ml/public/application/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,8 +23,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - +import { useMlKibana } from '../../../../contexts/kibana'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldDataCard } from '../field_data_card'; import { FieldTypesSelect } from '../field_types_select'; @@ -62,13 +61,17 @@ export const FieldsPanel: FC = ({ setFieldSearchBarQuery, fieldVisConfigs, }) => { + const { + services: { notifications }, + } = useMlKibana(); function onShowAllFieldsChange() { setShowAllFields(!showAllFields); } function onSearchBarChange(query: SearchBarQuery) { if (query.error) { - toastNotifications.addWarning( + const { toasts } = notifications; + toasts.addWarning( i18n.translate('xpack.ml.datavisualizer.fieldsPanel.searchBarError', { defaultMessage: `An error occurred running the search. {message}.`, values: { message: query.error.message }, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 53125f00c590e..3306533d8e2ca 100644 --- a/x-pack/legacy/plugins/ml/public/application/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,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; -import { SavedSearchQuery } from '../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../contexts/ml'; // @ts-ignore import { KqlFilterBar } from '../../../../components/kql_filter_bar/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 983908e2eb7f7..b0d8fa3d4fa88 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; -import { SavedSearchQuery } from '../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../contexts/ml'; import { IndexPatternTitle } from '../../../../../common/types/kibana'; import { ml } from '../../../services/ml_api_service'; @@ -92,6 +92,7 @@ export class DataLoader { } displayError(err: any) { + const toastNotifications = getToastNotifications(); if (err.statusCode === 500) { toastNotifications.addDanger( i18n.translate('xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage', { 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 index 268cd86da74fd..a6508ea868724 100644 --- 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 @@ -8,8 +8,6 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { merge } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { EuiFlexGroup, EuiFlexItem, @@ -29,6 +27,7 @@ import { esQuery, esKuery, } from '../../../../../../../../src/plugins/data/public'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; @@ -37,8 +36,9 @@ import { checkPermission } from '../../privilege/check_privilege'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; -import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; +import { useMlContext, SavedSearchQuery } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; +import { useMlKibana } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { TimeBuckets } from '../../util/time_buckets'; import { useUrlState } from '../../util/url_state'; @@ -97,12 +97,13 @@ function getDefaultPageState(): DataVisualizerPageState { } export const Page: FC = () => { - const kibanaContext = useKibanaContext(); + const { services } = useMlKibana(); + const mlContext = useMlContext(); - const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = kibanaContext; + const { timefilter } = services.data.query.timefilter; + const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = mlContext; const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); - const [globalState, setGlobalState] = useUrlState('_g'); useEffect(() => { if (globalState?.time !== undefined) { @@ -119,9 +120,6 @@ export const Page: FC = () => { }, [globalState?.refreshInterval?.pause, globalState?.refreshInterval?.value]); const [lastRefresh, setLastRefresh] = useState(0); - useEffect(() => { - loadOverallStats(); - }, [lastRefresh]); useEffect(() => { if (currentIndexPattern.timeFieldName !== undefined) { @@ -159,9 +157,15 @@ export const Page: FC = () => { mlNodesAvailable() && currentIndexPattern.timeFieldName !== undefined; - const [searchString, setSearchString] = useState(defaults.searchString); - const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); - const [searchQueryLanguage, setSearchQueryLanguage] = useState(defaults.searchQueryLanguage); + const { + searchQuery: initSearchQuery, + searchString: initSearchString, + queryLanguage: initQueryLanguage, + } = extractSearchData(currentSavedSearch); + + const [searchString, setSearchString] = useState(initSearchString); + const [searchQuery, setSearchQuery] = useState(initSearchQuery); + const [searchQueryLanguage] = useState(initQueryLanguage); const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); // TODO - type overallStats and stats @@ -208,30 +212,9 @@ export const Page: FC = () => { }; }); - useEffect(() => { - // Check for a saved search being passed in. - if (currentSavedSearch !== null) { - const { query } = getQueryFromSavedSearch(currentSavedSearch); - const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; - const qryString = query.query; - let qry; - if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { - const ast = esKuery.fromKueryExpression(qryString); - qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); - } else { - qry = esQuery.luceneStringToDsl(qryString); - esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); - } - - setSearchQuery(qry); - setSearchString(qryString); - setSearchQueryLanguage(queryLanguage); - } - }, []); - useEffect(() => { loadOverallStats(); - }, [searchQuery, samplerShardSize]); + }, [searchQuery, samplerShardSize, lastRefresh]); useEffect(() => { createMetricCards(); @@ -254,6 +237,37 @@ export const Page: FC = () => { createNonMetricCards(); }, [showAllNonMetrics, nonMetricShowFieldType, nonMetricFieldQuery]); + /** + * Extract query data from the saved search object. + */ + function extractSearchData(savedSearch: SavedSearchSavedObject | null) { + if (!savedSearch) { + return { + searchQuery: defaults.searchQuery, + searchString: defaults.searchString, + queryLanguage: defaults.searchQueryLanguage, + }; + } + + const { query } = getQueryFromSavedSearch(savedSearch); + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; + const qryString = query.query; + let qry; + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { + const ast = esKuery.fromKueryExpression(qryString); + qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); + } else { + qry = esQuery.luceneStringToDsl(qryString); + esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + } + + return { + searchQuery: qry, + searchString: qryString, + queryLanguage, + }; + } + async function loadOverallStats() { const tf = timefilter as any; let earliest; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 819db630c0609..37794a250db34 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -61,8 +61,6 @@ const memoizedLoadTopInfluencers = memoize(loadTopInfluencers); const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane); const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData); -const dateFormatTz = getDateFormatTz(); - export interface LoadExplorerDataConfig { bounds: TimeRangeBounds; influencersFilterQuery: any; @@ -121,6 +119,8 @@ function loadExplorerData(config: LoadExplorerDataConfig): Observable ({ @@ -255,6 +254,7 @@ export class Explorer extends React.Component { } catch (e) { console.log('Invalid kuery syntax', e); // eslint-disable-line no-console + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable', { defaultMessage: @@ -351,6 +351,7 @@ export class Explorer extends React.Component { viewBySwimlaneData.laneLabels && viewBySwimlaneData.laneLabels.length > 0; + const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index db9893a8a5c07..27b1278fa26db 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -28,7 +28,7 @@ exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` d.anomalyScore !== undefined); - highlight = highlight && highlight.entity; + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); - const filteredChartData = init(config); - drawRareChart(filteredChartData); + let vizWidth = 0; + const chartHeight = 170; + const LINE_CHART_ANOMALY_RADIUS = 7; + const SCHEDULED_EVENT_MARKER_HEIGHT = 5; - function init({ chartData }) { - const $el = $('.ml-explorer-chart'); + const chartType = getChartType(config); - // Clear any existing elements from the visualization, - // then build the svg elements for the chart. - const chartElement = d3.select(element).select('.content-wrapper'); - chartElement.select('svg').remove(); + // Left margin is adjusted later for longest y-axis label. + const margin = { top: 10, right: 0, bottom: 30, left: 0 }; + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + margin.left = 60; + } - const svgWidth = $el.width(); - const svgHeight = chartHeight + margin.top + margin.bottom; + let lineChartXScale = null; + let lineChartYScale = null; + let lineChartGroup; + let lineChartValuesLine = null; + + const CHART_Y_ATTRIBUTE = chartType === CHART_TYPE.EVENT_DISTRIBUTION ? 'entity' : 'value'; + + let highlight = config.chartData.find(d => d.anomalyScore !== undefined); + highlight = highlight && highlight.entity; + + const filteredChartData = init(config); + drawRareChart(filteredChartData); + + function init({ chartData }) { + const $el = $('.ml-explorer-chart'); + + // Clear any existing elements from the visualization, + // then build the svg elements for the chart. + const chartElement = d3.select(element).select('.content-wrapper'); + chartElement.select('svg').remove(); + + const svgWidth = $el.width(); + const svgHeight = chartHeight + margin.top + margin.bottom; + + const svg = chartElement + .append('svg') + .classed('ml-explorer-chart-svg', true) + .attr('width', svgWidth) + .attr('height', svgHeight); + + const categoryLimit = 30; + const scaleCategories = d3 + .nest() + .key(d => d.entity) + .entries(chartData) + .sort((a, b) => { + return b.values.length - a.values.length; + }) + .filter((d, i) => { + // only filter for rare charts + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { + return i < categoryLimit || d.key === highlight; + } + return true; + }) + .map(d => d.key); - const svg = chartElement - .append('svg') - .classed('ml-explorer-chart-svg', true) - .attr('width', svgWidth) - .attr('height', svgHeight); + chartData = chartData.filter(d => { + return scaleCategories.includes(d.entity); + }); - const categoryLimit = 30; - const scaleCategories = d3 - .nest() - .key(d => d.entity) - .entries(chartData) - .sort((a, b) => { - return b.values.length - a.values.length; - }) - .filter((d, i) => { - // only filter for rare charts - if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - return i < categoryLimit || d.key === highlight; - } - return true; + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + const focusData = chartData + .filter(d => { + return d.entity === highlight; }) - .map(d => d.key); + .map(d => d.value); + const focusExtent = d3.extent(focusData); + // now again filter chartData to include only the data points within the domain chartData = chartData.filter(d => { - return scaleCategories.includes(d.entity); + return d.value <= focusExtent[1]; }); - if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - const focusData = chartData - .filter(d => { - return d.entity === highlight; - }) - .map(d => d.value); - const focusExtent = d3.extent(focusData); - - // now again filter chartData to include only the data points within the domain - chartData = chartData.filter(d => { - return d.value <= focusExtent[1]; - }); - - lineChartYScale = d3.scale - .linear() - .range([chartHeight, 0]) - .domain([0, focusExtent[1]]) - .nice(); - } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - // avoid overflowing the border of the highlighted area - const rowMargin = 5; - lineChartYScale = d3.scale - .ordinal() - .rangePoints([rowMargin, chartHeight - rowMargin]) - .domain(scaleCategories); - } else { - throw `chartType '${chartType}' not supported`; - } + lineChartYScale = d3.scale + .linear() + .range([chartHeight, 0]) + .domain([0, focusExtent[1]]) + .nice(); + } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { + // avoid overflowing the border of the highlighted area + const rowMargin = 5; + lineChartYScale = d3.scale + .ordinal() + .rangePoints([rowMargin, chartHeight - rowMargin]) + .domain(scaleCategories); + } else { + throw `chartType '${chartType}' not supported`; + } - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - const tempLabelTextData = - chartType === CHART_TYPE.POPULATION_DISTRIBUTION - ? lineChartYScale.ticks() - : scaleCategories; - tempLabelText - .selectAll('text.temp.axis') - .data(tempLabelTextData) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - return lineChartYScale.tickFormat()(d); - } - return d; + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + const tempLabelTextData = + chartType === CHART_TYPE.POPULATION_DISTRIBUTION + ? lineChartYScale.ticks() + : scaleCategories; + tempLabelText + .selectAll('text.temp.axis') + .data(tempLabelTextData) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + return lineChartYScale.tickFormat()(d); } - }) - // Don't use an arrow function since we need access to `this`. - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + yAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - // Set the size of the left margin according to the width of the largest y axis tick label - // if the chart is either a population chart or a rare chart below the cardinality threshold. - if ( - chartType === CHART_TYPE.POPULATION_DISTRIBUTION || - (chartType === CHART_TYPE.EVENT_DISTRIBUTION && - scaleCategories.length <= Y_AXIS_LABEL_THRESHOLD) - ) { - margin.left = Math.max(maxYAxisLabelWidth, 40); - } - vizWidth = svgWidth - margin.left - margin.right; - - // Set the x axis domain to match the request plot range. - // This ensures ranges on different charts will match, even when there aren't - // data points across the full range, and the selected anomalous region is centred. - lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([config.plotEarliest, config.plotLatest]); - - lineChartValuesLine = d3.svg - .line() - .x(d => lineChartXScale(d.date)) - .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) - .defined(d => d.value !== null); - - lineChartGroup = svg - .append('g') - .attr('class', 'line-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - return chartData; + return d; + } + }) + // Don't use an arrow function since we need access to `this`. + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + yAxis.tickPadding(), + maxYAxisLabelWidth + ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + // Set the size of the left margin according to the width of the largest y axis tick label + // if the chart is either a population chart or a rare chart below the cardinality threshold. + if ( + chartType === CHART_TYPE.POPULATION_DISTRIBUTION || + (chartType === CHART_TYPE.EVENT_DISTRIBUTION && + scaleCategories.length <= Y_AXIS_LABEL_THRESHOLD) + ) { + margin.left = Math.max(maxYAxisLabelWidth, 40); } + vizWidth = svgWidth - margin.left - margin.right; + + // Set the x axis domain to match the request plot range. + // This ensures ranges on different charts will match, even when there aren't + // data points across the full range, and the selected anomalous region is centred. + lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([config.plotEarliest, config.plotLatest]); + + lineChartValuesLine = d3.svg + .line() + .x(d => lineChartXScale(d.date)) + .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) + .defined(d => d.value !== null); + + lineChartGroup = svg + .append('g') + .attr('class', 'line-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + return chartData; + } + + function drawRareChart(data) { + // Add border round plot area. + lineChartGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('height', chartHeight) + .attr('width', vizWidth) + .style('stroke', '#cccccc') + .style('fill', 'none') + .style('stroke-width', 1); + + drawRareChartAxes(); + drawRareChartHighlightedSpan(); + drawRareChartDots(data, lineChartGroup, lineChartValuesLine); + drawRareChartMarkers(data); + } + + function drawRareChartAxes() { + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('auto'); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); + // +1 ms to account for the ms that was subtracted for query aggregations. + const interval = config.selectedLatest - config.selectedEarliest + 1; + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); - function drawRareChart(data) { - // Add border round plot area. - lineChartGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('height', chartHeight) - .attr('width', vizWidth) - .style('stroke', '#cccccc') - .style('fill', 'none') - .style('stroke-width', 1); - - drawRareChartAxes(); - drawRareChartHighlightedSpan(); - drawRareChartDots(data, lineChartGroup, lineChartValuesLine); - drawRareChartMarkers(data); + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + // With tooManyBuckets the chart would end up with no x-axis labels + // because the ticks are based on the span of the emphasis section, + // and the highlighted area spans the whole chart. + if (tooManyBuckets === false) { + xAxis.tickValues(tickValues); + } else { + xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); } - function drawRareChartAxes() { - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; - timeBuckets.setBounds(bounds); - timeBuckets.setInterval('auto'); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was subtracted for query aggregations. - const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); - - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { - xAxis.tickValues(tickValues); - } else { - xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); - } + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); + if (fieldFormat !== undefined) { + yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); + } - if (fieldFormat !== undefined) { - yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); - } + const axes = lineChartGroup.append('g'); - const axes = lineChartGroup.append('g'); + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); + // emphasize the y axis label this rare chart is actually about + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); - - // emphasize the y axis label this rare chart is actually about - if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - axes - .select('.y') - .selectAll('text') - .each(function(d) { - d3.select(this).classed('ml-explorer-chart-axis-emphasis', d === highlight); - }); - } + .select('.y') + .selectAll('text') + .each(function(d) { + d3.select(this).classed('ml-explorer-chart-axis-emphasis', d === highlight); + }); + } - if (tooManyBuckets === false) { - removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); - } + if (tooManyBuckets === false) { + removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } + } - function drawRareChartDots(dotsData, rareChartGroup, rareChartValuesLine, radius = 1.5) { - // check if `g.values-dots` already exists, if not create it - // in both cases assign the element to `dotGroup` - const dotGroup = rareChartGroup.select('.values-dots').empty() - ? rareChartGroup.append('g').classed('values-dots', true) - : rareChartGroup.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') - .classed('values-dots-circle', true) - .classed('values-dots-circle-blur', d => { - return d.entity !== highlight; - }) - .attr('r', d => (d.entity === highlight ? radius * 1.5 : radius)); + function drawRareChartDots(dotsData, rareChartGroup, rareChartValuesLine, radius = 1.5) { + // check if `g.values-dots` already exists, if not create it + // in both cases assign the element to `dotGroup` + const dotGroup = rareChartGroup.select('.values-dots').empty() + ? rareChartGroup.append('g').classed('values-dots', true) + : rareChartGroup.select('.values-dots'); - dots.attr('cx', rareChartValuesLine.x()).attr('cy', rareChartValuesLine.y()); + // use d3's enter/update/exit pattern to render the dots + const dots = dotGroup.selectAll('circle').data(dotsData); - dots.exit().remove(); - } + dots + .enter() + .append('circle') + .classed('values-dots-circle', true) + .classed('values-dots-circle-blur', d => { + return d.entity !== highlight; + }) + .attr('r', d => (d.entity === highlight ? radius * 1.5 : radius)); - function drawRareChartHighlightedSpan() { - // Draws a rectangle which highlights the time span that has been selected for view. - // Note depending on the overall time range and the bucket span, the selected time - // span may be longer than the range actually being plotted. - const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); - const rectEnd = Math.min(config.selectedLatest, config.plotLatest); - const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); - - lineChartGroup - .append('rect') - .attr('class', 'selected-interval') - .attr('x', lineChartXScale(new Date(rectStart)) + 2) - .attr('y', 2) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', rectWidth - 4) - .attr('height', chartHeight - 4); - } + dots.attr('cx', rareChartValuesLine.x()).attr('cy', rareChartValuesLine.y()); - function drawRareChartMarkers(data) { - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps) - const dots = lineChartGroup - .append('g') - .attr('class', 'chart-markers') - .selectAll('.metric-value') - .data(data.filter(d => d.value !== null)); - - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - dots - .attr('cx', d => lineChartXScale(d.date)) - .attr('cy', d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) - .attr('class', d => { - let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) { - markerClass += ' anomaly-marker '; - markerClass += getSeverityWithLow(d.anomalyScore).id; - } - return markerClass; - }); + dots.exit().remove(); + } - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_MARKER_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr( - 'y', - d => lineChartYScale(d[CHART_Y_ATTRIBUTE]) - SCHEDULED_EVENT_MARKER_HEIGHT / 2 - ); - } + function drawRareChartHighlightedSpan() { + // Draws a rectangle which highlights the time span that has been selected for view. + // Note depending on the overall time range and the bucket span, the selected time + // span may be longer than the range actually being plotted. + const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); + const rectEnd = Math.min(config.selectedLatest, config.plotLatest); + const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); + + lineChartGroup + .append('rect') + .attr('class', 'selected-interval') + .attr('x', lineChartXScale(new Date(rectStart)) + 2) + .attr('y', 2) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', rectWidth - 4) + .attr('height', chartHeight - 4); + } - function showLineChartTooltip(marker, circle) { - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTime(marker.date); - const tooltipData = [{ name: formattedDate }]; - const seriesKey = config.detectorLabel; + function drawRareChartMarkers(data) { + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps) + const dots = lineChartGroup + .append('g') + .attr('class', 'chart-markers') + .selectAll('.metric-value') + .data(data.filter(d => d.value !== null)); + + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + dots + .attr('cx', d => lineChartXScale(d.date)) + .attr('cy', d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) + .attr('class', d => { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) { + markerClass += ' anomaly-marker '; + markerClass += getSeverityWithLow(d.anomalyScore).id; + } + return markerClass; + }); - if (_.has(marker, 'entity')) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.entityLabel', - defaultMessage: 'entity', - }), - value: marker.entity, - seriesKey, - }); - } + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_MARKER_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => lineChartYScale(d[CHART_Y_ATTRIBUTE]) - SCHEDULED_EVENT_MARKER_HEIGHT / 2); + } - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + function showLineChartTooltip(marker, circle) { + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTime(marker.date); + const tooltipData = [{ name: formattedDate }]; + const seriesKey = config.detectorLabel; + + if (_.has(marker, 'entity')) { + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.distributionChart.entityLabel', { + defaultMessage: 'entity', + }), + value: marker.entity, + seriesKey, + }); + } + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.distributionChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: getSeverityColor(score), + seriesKey, + yAccessor: 'anomaly_score', + }); + if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', + name: i18n.translate('xpack.ml.explorer.distributionChart.valueLabel', { + defaultMessage: 'value', }), - value: displayScore, - color: getSeverityColor(score), + value: formatValue(marker.value, config.functionDescription, fieldFormat), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'value', }); - if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { + if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.valueLabel', - defaultMessage: 'value', + name: i18n.translate('xpack.ml.explorer.distributionChart.typicalLabel', { + defaultMessage: 'typical', }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), + value: formatValue(marker.typical, config.functionDescription, fieldFormat), seriesKey, - yAccessor: 'value', + yAccessor: 'typical', }); - if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } - if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', - defaultMessage: - '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', - }, - { + } + if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', + { + defaultMessage: + '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', + values: { numberOfCauses: marker.numberOfCauses, byFieldName: marker.byFieldName, // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. plusSign: marker.numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } - } else if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - } - - if (_.has(marker, 'scheduledEvents')) { - marker.scheduledEvents.forEach((scheduledEvent, i) => { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', - defaultMessage: 'scheduled event{counter}', - }, - { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' } + }, + } ), - value: scheduledEvent, seriesKey, - yAccessor: `scheduled_events_${i + 1}`, + yAccessor: 'numberOfCauses', }); - }); + } } - - mlChartTooltipService.show(tooltipData, circle, { - x: LINE_CHART_ANOMALY_RADIUS * 3, - y: LINE_CHART_ANOMALY_RADIUS * 2, + } else if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - } - shouldComponentUpdate() { - // Always return true, d3 will take care of appropriate re-rendering. - return true; - } + if (_.has(marker, 'scheduledEvents')) { + marker.scheduledEvents.forEach((scheduledEvent, i) => { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', + { + defaultMessage: 'scheduled event{counter}', + values: { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' }, + } + ), + value: scheduledEvent, + seriesKey, + yAccessor: `scheduled_events_${i + 1}`, + }); + }); + } - setRef(componentNode) { - this.rootNode = componentNode; + mlChartTooltipService.show(tooltipData, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 3, + y: LINE_CHART_ANOMALY_RADIUS * 2, + }); } + } - render() { - const { seriesConfig } = this.props; + shouldComponentUpdate() { + // Always return true, d3 will take care of appropriate re-rendering. + return true; + } - if (typeof seriesConfig === 'undefined') { - // just return so the empty directive renders without an error later on - return null; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - // create a chart loading placeholder - const isLoading = seriesConfig.loading; + render() { + const { seriesConfig } = this.props; - return ( -

- {isLoading && } - {!isLoading &&
} -
- ); + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ {isLoading && } + {!isLoading &&
} +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 313399b0260bc..71d777db5b2ec 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_chart_distribution.test.mocks'; import { chartData as mockChartData } from './__mocks__/mock_chart_data_rare'; import seriesConfig from './__mocks__/mock_series_config_rare.json'; @@ -22,12 +21,6 @@ jest.mock('../../services/field_format_service', () => ({ getFieldFormat: jest.fn(), }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -51,9 +44,7 @@ describe('ExplorerChart', () => { test('Initialize', () => { const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -69,7 +60,7 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - @@ -95,7 +86,7 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl(
- diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js index 5aab26f707252..5cf8245cd4739 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -10,7 +10,6 @@ import React from 'react'; import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; const CHART_DESCRIPTION = { [CHART_TYPE.EVENT_DISTRIBUTION]: i18n.translate( @@ -47,34 +46,30 @@ function TooltipDefinitionList({ toolTipData }) { ); } -export const ExplorerChartInfoTooltip = injectI18n(function ExplorerChartInfoTooltip({ +export const ExplorerChartInfoTooltip = ({ jobId, aggregationInterval, chartFunction, chartType, entityFields = [], - intl, -}) { +}) => { const chartDescription = CHART_DESCRIPTION[chartType]; const toolTipData = [ { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.jobIdTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.jobIdTitle', { defaultMessage: 'job ID', }), description: jobId, }, { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.aggregationIntervalTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.aggregationIntervalTitle', { defaultMessage: 'aggregation interval', }), description: aggregationInterval, }, { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.chartFunctionTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.chartFunctionTitle', { defaultMessage: 'chart function', }), description: chartFunction, @@ -99,8 +94,8 @@ export const ExplorerChartInfoTooltip = injectI18n(function ExplorerChartInfoToo )}
); -}); -ExplorerChartInfoTooltip.WrappedComponent.propTypes = { +}; +ExplorerChartInfoTooltip.propTypes = { jobId: PropTypes.string.isRequired, aggregationInterval: PropTypes.string, chartFunction: PropTypes.string, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 32b39131a9ae2..632c5a1006df5 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -23,7 +23,7 @@ describe('ExplorerChartTooltip', () => { jobId: 'mock-job-id', }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index a255b6b0434e4..d8d6709175090 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -43,490 +43,480 @@ import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; -export const ExplorerChartSingleMetric = injectI18n( - class ExplorerChartSingleMetric extends React.Component { - static propTypes = { - tooManyBuckets: PropTypes.bool, - seriesConfig: PropTypes.object, - severity: PropTypes.number.isRequired, - }; +export class ExplorerChartSingleMetric extends React.Component { + static propTypes = { + tooManyBuckets: PropTypes.bool, + seriesConfig: PropTypes.object, + severity: PropTypes.number.isRequired, + }; - componentDidMount() { - this.renderChart(); - } + componentDidMount() { + this.renderChart(); + } - componentDidUpdate() { - this.renderChart(); - } + componentDidUpdate() { + this.renderChart(); + } - renderChart() { - const { tooManyBuckets, intl } = this.props; + renderChart() { + const { tooManyBuckets } = this.props; - const element = this.rootNode; - const config = this.props.seriesConfig; - const severity = this.props.severity; + const element = this.rootNode; + const config = this.props.seriesConfig; + const severity = this.props.severity; - if (typeof config === 'undefined' || Array.isArray(config.chartData) === false) { - // just return so the empty directive renders without an error later on - return; - } + if (typeof config === 'undefined' || Array.isArray(config.chartData) === false) { + // just return so the empty directive renders without an error later on + return; + } - const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); - - let vizWidth = 0; - const chartHeight = 170; - - // Left margin is adjusted later for longest y-axis label. - const margin = { top: 10, right: 0, bottom: 30, left: 60 }; - - let lineChartXScale = null; - let lineChartYScale = null; - let lineChartGroup; - let lineChartValuesLine = null; - - init(config.chartLimits); - drawLineChart(config.chartData); - - function init(chartLimits) { - const $el = $('.ml-explorer-chart'); - - // Clear any existing elements from the visualization, - // then build the svg elements for the chart. - const chartElement = d3.select(element).select(`.${CONTENT_WRAPPER_CLASS}`); - chartElement.select('svg').remove(); - - const svgWidth = $el.width(); - const svgHeight = chartHeight + margin.top + margin.bottom; - - const svg = chartElement - .append('svg') - .classed('ml-explorer-chart-svg', true) - .attr('width', svgWidth) - .attr('height', svgHeight); - - // Set the size of the left margin according to the width of the largest y axis tick label. - lineChartYScale = d3.scale - .linear() - .range([chartHeight, 0]) - .domain([chartLimits.min, chartLimits.max]) - .nice(); - - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - tempLabelText - .selectAll('text.temp.axis') - .data(lineChartYScale.ticks()) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - return lineChartYScale.tickFormat()(d); - } - }) - // Don't use an arrow function since we need access to `this`. - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + yAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - margin.left = Math.max(maxYAxisLabelWidth, 40); - vizWidth = svgWidth - margin.left - margin.right; - - // Set the x axis domain to match the request plot range. - // This ensures ranges on different charts will match, even when there aren't - // data points across the full range, and the selected anomalous region is centred. - lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([config.plotEarliest, config.plotLatest]); - - lineChartValuesLine = d3.svg - .line() - .x(d => lineChartXScale(d.date)) - .y(d => lineChartYScale(d.value)) - .defined(d => d.value !== null); - - lineChartGroup = svg - .append('g') - .attr('class', 'line-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - } + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); + + let vizWidth = 0; + const chartHeight = 170; + + // Left margin is adjusted later for longest y-axis label. + const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + + let lineChartXScale = null; + let lineChartYScale = null; + let lineChartGroup; + let lineChartValuesLine = null; + + init(config.chartLimits); + drawLineChart(config.chartData); + + function init(chartLimits) { + const $el = $('.ml-explorer-chart'); + + // Clear any existing elements from the visualization, + // then build the svg elements for the chart. + const chartElement = d3.select(element).select(`.${CONTENT_WRAPPER_CLASS}`); + chartElement.select('svg').remove(); + + const svgWidth = $el.width(); + const svgHeight = chartHeight + margin.top + margin.bottom; + + const svg = chartElement + .append('svg') + .classed('ml-explorer-chart-svg', true) + .attr('width', svgWidth) + .attr('height', svgHeight); + + // Set the size of the left margin according to the width of the largest y axis tick label. + lineChartYScale = d3.scale + .linear() + .range([chartHeight, 0]) + .domain([chartLimits.min, chartLimits.max]) + .nice(); + + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + tempLabelText + .selectAll('text.temp.axis') + .data(lineChartYScale.ticks()) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return lineChartYScale.tickFormat()(d); + } + }) + // Don't use an arrow function since we need access to `this`. + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + yAxis.tickPadding(), + maxYAxisLabelWidth + ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = Math.max(maxYAxisLabelWidth, 40); + vizWidth = svgWidth - margin.left - margin.right; + + // Set the x axis domain to match the request plot range. + // This ensures ranges on different charts will match, even when there aren't + // data points across the full range, and the selected anomalous region is centred. + lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([config.plotEarliest, config.plotLatest]); + + lineChartValuesLine = d3.svg + .line() + .x(d => lineChartXScale(d.date)) + .y(d => lineChartYScale(d.value)) + .defined(d => d.value !== null); + + lineChartGroup = svg + .append('g') + .attr('class', 'line-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + } - function drawLineChart(data) { - // Add border round plot area. - lineChartGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('height', chartHeight) - .attr('width', vizWidth) - .style('stroke', '#cccccc') - .style('fill', 'none') - .style('stroke-width', 1); - - drawLineChartAxes(); - drawLineChartHighlightedSpan(); - drawLineChartPaths(data); - drawLineChartDots(data, lineChartGroup, lineChartValuesLine); - drawLineChartMarkers(data); - } + function drawLineChart(data) { + // Add border round plot area. + lineChartGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('height', chartHeight) + .attr('width', vizWidth) + .style('stroke', '#cccccc') + .style('fill', 'none') + .style('stroke-width', 1); + + drawLineChartAxes(); + drawLineChartHighlightedSpan(); + drawLineChartPaths(data); + drawLineChartDots(data, lineChartGroup, lineChartValuesLine); + drawLineChartMarkers(data); + } - function drawLineChartAxes() { - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; - timeBuckets.setBounds(bounds); - timeBuckets.setInterval('auto'); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was subtracted for query aggregations. - const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); + function drawLineChartAxes() { + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('auto'); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); + // +1 ms to account for the ms that was subtracted for query aggregations. + const interval = config.selectedLatest - config.selectedEarliest + 1; + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { - xAxis.tickValues(tickValues); - } else { - xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); - } + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + // With tooManyBuckets the chart would end up with no x-axis labels + // because the ticks are based on the span of the emphasis section, + // and the highlighted area spans the whole chart. + if (tooManyBuckets === false) { + xAxis.tickValues(tickValues); + } else { + xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); + } - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); - if (fieldFormat !== undefined) { - yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); - } + if (fieldFormat !== undefined) { + yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); + } - const axes = lineChartGroup.append('g'); + const axes = lineChartGroup.append('g'); - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); - if (tooManyBuckets === false) { - removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); - } + if (tooManyBuckets === false) { + removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } + } - function drawLineChartHighlightedSpan() { - // Draws a rectangle which highlights the time span that has been selected for view. - // Note depending on the overall time range and the bucket span, the selected time - // span may be longer than the range actually being plotted. - const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); - const rectEnd = Math.min(config.selectedLatest, config.plotLatest); - const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); - - lineChartGroup - .append('rect') - .attr('class', 'selected-interval') - .attr('x', lineChartXScale(new Date(rectStart)) + 2) - .attr('y', 2) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', rectWidth - 4) - .attr('height', chartHeight - 4); - } + function drawLineChartHighlightedSpan() { + // Draws a rectangle which highlights the time span that has been selected for view. + // Note depending on the overall time range and the bucket span, the selected time + // span may be longer than the range actually being plotted. + const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); + const rectEnd = Math.min(config.selectedLatest, config.plotLatest); + const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); + + lineChartGroup + .append('rect') + .attr('class', 'selected-interval') + .attr('x', lineChartXScale(new Date(rectStart)) + 2) + .attr('y', 2) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', rectWidth - 4) + .attr('height', chartHeight - 4); + } - function drawLineChartPaths(data) { - lineChartGroup - .append('path') - .attr('class', 'values-line') - .attr('d', lineChartValuesLine(data)); - } + function drawLineChartPaths(data) { + lineChartGroup + .append('path') + .attr('class', 'values-line') + .attr('d', lineChartValuesLine(data)); + } - function drawLineChartMarkers(data) { - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps, with no anomalies) - // or for multi-bucket anomalies. - const dots = lineChartGroup - .append('g') - .attr('class', 'chart-markers') - .selectAll('.metric-value') - .data( - data.filter( - d => - (d.value !== null || typeof d.anomalyScore === 'number') && - !showMultiBucketAnomalyMarker(d) - ) - ); + function drawLineChartMarkers(data) { + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps, with no anomalies) + // or for multi-bucket anomalies. + const dots = lineChartGroup + .append('g') + .attr('class', 'chart-markers') + .selectAll('.metric-value') + .data( + data.filter( + d => + (d.value !== null || typeof d.anomalyScore === 'number') && + !showMultiBucketAnomalyMarker(d) + ) + ); - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - const isAnomalyVisible = d => - _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; - - // Update all dots to new positions. - dots - .attr('cx', d => lineChartXScale(d.date)) - .attr('cy', d => lineChartYScale(d.value)) - .attr('class', d => { - let markerClass = 'metric-value'; - if (isAnomalyVisible(d)) { - markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; - } - return markerClass; - }); + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + const isAnomalyVisible = d => _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; + + // Update all dots to new positions. + dots + .attr('cx', d => lineChartXScale(d.date)) + .attr('cy', d => lineChartYScale(d.value)) + .attr('class', d => { + let markerClass = 'metric-value'; + if (isAnomalyVisible(d)) { + markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; + } + return markerClass; + }); - // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.multi-bucket') - .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true)); - - // Remove multi-bucket markers that are no longer needed - multiBucketMarkers.exit().remove(); - - // Append the multi-bucket markers and position on chart. - multiBucketMarkers - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .attr( - 'transform', - d => `translate(${lineChartXScale(d.date)}, ${lineChartYScale(d.value)})` - ) - .attr( - 'class', - d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}` - ) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr('y', d => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2); - } + // Render cross symbols for any multi-bucket anomalies. + const multiBucketMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.multi-bucket') + .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true)); + + // Remove multi-bucket markers that are no longer needed + multiBucketMarkers.exit().remove(); + + // Append the multi-bucket markers and position on chart. + multiBucketMarkers + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .attr( + 'transform', + d => `translate(${lineChartXScale(d.date)}, ${lineChartYScale(d.value)})` + ) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2); + } - function showLineChartTooltip(marker, circle) { - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTime(marker.date); - const tooltipData = [{ name: formattedDate }]; - const seriesKey = config.detectorLabel; + function showLineChartTooltip(marker, circle) { + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTime(marker.date); + const tooltipData = [{ name: formattedDate }]; + const seriesKey = config.detectorLabel; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: getSeverityColor(score), + seriesKey, + yAccessor: 'anomaly_score', + }); - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + if (showMultiBucketAnomalyTooltip(marker) === true) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', + name: i18n.translate('xpack.ml.explorer.singleMetricChart.multiBucketImpactLabel', { + defaultMessage: 'multi-bucket impact', }), - value: displayScore, - color: getSeverityColor(score), + value: getMultiBucketImpactLabel(marker.multiBucketImpact), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'multi_bucket_impact', }); + } - if (showMultiBucketAnomalyTooltip(marker) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.multiBucketImpactLabel', - defaultMessage: 'multi-bucket impact', - }), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), - seriesKey, - yAccessor: 'multi_bucket_impact', - }); - } - - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.actualLabel', - defaultMessage: 'actual', - }), - value: formatValue(marker.actual, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'actual', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } else { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.valueLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', - defaultMessage: - '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', - }, - { - numberOfCauses: marker.numberOfCauses, - byFieldName: marker.byFieldName, - // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. - plusSign: marker.numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.actualLabel', { + defaultMessage: 'actual', + }), + value: formatValue(marker.actual, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'actual', + }); + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.typicalLabel', { + defaultMessage: 'typical', + }), + value: formatValue(marker.typical, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'typical', + }); } else { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel', + name: i18n.translate('xpack.ml.explorer.singleMetricChart.valueLabel', { defaultMessage: 'value', }), value: formatValue(marker.value, config.functionDescription, fieldFormat), seriesKey, yAccessor: 'value', }); + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', + { + defaultMessage: + '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', + values: { + numberOfCauses: marker.numberOfCauses, + byFieldName: marker.byFieldName, + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + plusSign: marker.numberOfCauses < 10 ? '' : '+', + }, + } + ), + seriesKey, + yAccessor: 'numberOfCauses', + }); + } } - - if (_.has(marker, 'scheduledEvents')) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.scheduledEventsLabel', - defaultMessage: 'Scheduled events', - }), - value: marker.scheduledEvents.map(mlEscape).join('
'), - }); - } - - mlChartTooltipService.show(tooltipData, circle, { - x: LINE_CHART_ANOMALY_RADIUS * 3, - y: LINE_CHART_ANOMALY_RADIUS * 2, + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - } - shouldComponentUpdate() { - // Always return true, d3 will take care of appropriate re-rendering. - return true; - } + if (_.has(marker, 'scheduledEvents')) { + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.scheduledEventsLabel', { + defaultMessage: 'Scheduled events', + }), + value: marker.scheduledEvents.map(mlEscape).join('
'), + }); + } - setRef(componentNode) { - this.rootNode = componentNode; + mlChartTooltipService.show(tooltipData, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 3, + y: LINE_CHART_ANOMALY_RADIUS * 2, + }); } + } - render() { - const { seriesConfig } = this.props; + shouldComponentUpdate() { + // Always return true, d3 will take care of appropriate re-rendering. + return true; + } - if (typeof seriesConfig === 'undefined') { - // just return so the empty directive renders without an error later on - return null; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - // create a chart loading placeholder - const isLoading = seriesConfig.loading; + render() { + const { seriesConfig } = this.props; - return ( -
- {isLoading && } - {!isLoading &&
} -
- ); + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ {isLoading && } + {!isLoading &&
} +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index d291dbb23d016..ca3e52308a936 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_chart_single_metric.test.mocks'; import { chartData as mockChartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; @@ -22,12 +21,6 @@ jest.mock('../../services/field_format_service', () => ({ getFieldFormat: jest.fn(), }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -51,9 +44,7 @@ describe('ExplorerChart', () => { test('Initialize', () => { const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -69,7 +60,7 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - @@ -95,7 +86,7 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl(
- diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 4b2d307e72c66..3a6c8c8790def 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -14,7 +14,6 @@ import { chartLimits } from '../../util/chart_utils'; import { getDefaultChartsData } from './explorer_charts_container_service'; import { ExplorerChartsContainer } from './explorer_charts_container'; -import './explorer_chart_single_metric.test.mocks'; import { chartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; import seriesConfigRare from './__mocks__/mock_series_config_rare.json'; @@ -39,22 +38,6 @@ jest.mock('../../services/job_service', () => ({ }, })); -// The mocks for ui/chrome and ui/timefilter are copied from charts_utils.test.js -// TODO: Refactor the involved tests to avoid this duplication -jest.mock( - 'ui/chrome', - () => ({ - addBasePath: () => '/api/ml', - getBasePath: () => { - return ''; - }, - getInjected: () => true, - }), - { virtual: true } -); - -jest.mock('ui/new_platform'); - describe('ExplorerChartsContainer', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; diff --git a/x-pack/legacy/plugins/ml/public/application/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 deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts +++ /dev/null @@ -1,9 +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('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index fbbf5eb324095..35261257ce625 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_charts_container_service.test.mocks'; import _ from 'lodash'; import mockAnomalyChartRecords from './__mocks__/mock_anomaly_chart_records.json'; @@ -95,13 +94,6 @@ jest.mock('../legacy_utils', () => ({ }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); - jest.mock('../explorer_dashboard_service', () => ({ explorerService: { setCharts: jest.fn(), diff --git a/x-pack/legacy/plugins/ml/public/application/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 deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts +++ /dev/null @@ -1,9 +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('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js index 7ae9d215d7034..6582f5c609864 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -24,7 +24,6 @@ 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'; import { DRAG_SELECT_ACTION } from './explorer_constants'; -import { injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; const SCSS = { @@ -32,581 +31,574 @@ const SCSS = { mlHideRangeSelection: 'mlHideRangeSelection', }; -export const ExplorerSwimlane = injectI18n( - class ExplorerSwimlane extends React.Component { - static propTypes = { - chartWidth: PropTypes.number.isRequired, - filterActive: PropTypes.bool, - maskAll: PropTypes.bool, - TimeBuckets: PropTypes.func.isRequired, - swimlaneCellClick: PropTypes.func.isRequired, - swimlaneData: PropTypes.shape({ - laneLabels: PropTypes.array.isRequired, - }).isRequired, - swimlaneType: PropTypes.string.isRequired, - selection: PropTypes.object, - swimlaneRenderDoneListener: PropTypes.func.isRequired, - }; +export class ExplorerSwimlane extends React.Component { + static propTypes = { + chartWidth: PropTypes.number.isRequired, + filterActive: PropTypes.bool, + maskAll: PropTypes.bool, + TimeBuckets: PropTypes.func.isRequired, + swimlaneCellClick: PropTypes.func.isRequired, + swimlaneData: PropTypes.shape({ + laneLabels: PropTypes.array.isRequired, + }).isRequired, + swimlaneType: PropTypes.string.isRequired, + selection: PropTypes.object, + swimlaneRenderDoneListener: PropTypes.func.isRequired, + }; + + // Since this component is mostly rendered using d3 and cellMouseoverActive is only + // relevant for d3 based interaction, we don't manage this using React's state + // and intentionally circumvent the component lifecycle when updating it. + cellMouseoverActive = true; + + dragSelectSubscriber = null; + + componentDidMount() { + // property for data comparison to be able to filter + // consecutive click events with the same data. + let previousSelectedData = null; + + // Listen for dragSelect events + this.dragSelectSubscriber = dragSelect$.subscribe(({ action, elements = [] }) => { + const element = d3.select(this.rootNode.parentNode); + const { swimlaneType } = this.props; - // Since this component is mostly rendered using d3 and cellMouseoverActive is only - // relevant for d3 based interaction, we don't manage this using React's state - // and intentionally circumvent the component lifecycle when updating it. - cellMouseoverActive = true; - - dragSelectSubscriber = null; - - componentDidMount() { - // property for data comparison to be able to filter - // consecutive click events with the same data. - let previousSelectedData = null; - - // Listen for dragSelect events - this.dragSelectSubscriber = dragSelect$.subscribe(({ action, elements = [] }) => { - const element = d3.select(this.rootNode.parentNode); - const { swimlaneType } = this.props; - - if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { - element.classed(SCSS.mlDragselectDragging, false); - const firstSelectedCell = d3.select(elements[0]).node().__clickData__; - - if ( - typeof firstSelectedCell !== 'undefined' && - swimlaneType === firstSelectedCell.swimlaneType - ) { - const selectedData = elements.reduce( - (d, e) => { - const cell = d3.select(e).node().__clickData__; - d.bucketScore = Math.max(d.bucketScore, cell.bucketScore); - d.laneLabels.push(cell.laneLabel); - d.times.push(cell.time); - return d; - }, - { - bucketScore: 0, - laneLabels: [], - times: [], - } - ); - - selectedData.laneLabels = _.uniq(selectedData.laneLabels); - selectedData.times = _.uniq(selectedData.times); - if (_.isEqual(selectedData, previousSelectedData) === false) { - // If no cells containing anomalies have been selected, - // immediately clear the selection, otherwise trigger - // a reload with the updated selected cells. - if (selectedData.bucketScore === 0) { - elements.map(e => d3.select(e).classed('ds-selected', false)); - this.selectCell([], selectedData); - previousSelectedData = null; - } else { - this.selectCell(elements, selectedData); - previousSelectedData = selectedData; - } + if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { + element.classed(SCSS.mlDragselectDragging, false); + const firstSelectedCell = d3.select(elements[0]).node().__clickData__; + + if ( + typeof firstSelectedCell !== 'undefined' && + swimlaneType === firstSelectedCell.swimlaneType + ) { + const selectedData = elements.reduce( + (d, e) => { + const cell = d3.select(e).node().__clickData__; + d.bucketScore = Math.max(d.bucketScore, cell.bucketScore); + d.laneLabels.push(cell.laneLabel); + d.times.push(cell.time); + return d; + }, + { + bucketScore: 0, + laneLabels: [], + times: [], } - } + ); - this.cellMouseoverActive = true; - } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { - element.classed(SCSS.mlDragselectDragging, true); - } else if (action === DRAG_SELECT_ACTION.DRAG_START) { - previousSelectedData = null; - this.cellMouseoverActive = false; - mlChartTooltipService.hide(true); + selectedData.laneLabels = _.uniq(selectedData.laneLabels); + selectedData.times = _.uniq(selectedData.times); + if (_.isEqual(selectedData, previousSelectedData) === false) { + // If no cells containing anomalies have been selected, + // immediately clear the selection, otherwise trigger + // a reload with the updated selected cells. + if (selectedData.bucketScore === 0) { + elements.map(e => d3.select(e).classed('ds-selected', false)); + this.selectCell([], selectedData); + previousSelectedData = null; + } else { + this.selectCell(elements, selectedData); + previousSelectedData = selectedData; + } + } } - }); - - this.renderSwimlane(); - } - - componentDidUpdate() { - this.renderSwimlane(); - } - componentWillUnmount() { - if (this.dragSelectSubscriber !== null) { - this.dragSelectSubscriber.unsubscribe(); + this.cellMouseoverActive = true; + } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { + element.classed(SCSS.mlDragselectDragging, true); + } else if (action === DRAG_SELECT_ACTION.DRAG_START) { + previousSelectedData = null; + this.cellMouseoverActive = false; + mlChartTooltipService.hide(true); } - const element = d3.select(this.rootNode); - element.html(''); - } + }); - selectCell(cellsToSelect, { laneLabels, bucketScore, times }) { - const { selection, swimlaneCellClick, swimlaneData, swimlaneType } = this.props; + this.renderSwimlane(); + } - let triggerNewSelection = false; + componentDidUpdate() { + this.renderSwimlane(); + } - if (cellsToSelect.length > 1 || bucketScore > 0) { - triggerNewSelection = true; - } + componentWillUnmount() { + if (this.dragSelectSubscriber !== null) { + this.dragSelectSubscriber.unsubscribe(); + } + const element = d3.select(this.rootNode); + element.html(''); + } - // Check if the same cells were selected again, if so clear the selection, - // otherwise activate the new selection. The two objects are built for - // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" - // since it also includes the "viewBy" attribute which might differ depending - // on whether the overall or viewby swimlane was selected. - const oldSelection = { - selectedType: selection && selection.type, - selectedLanes: selection && selection.lanes, - selectedTimes: selection && selection.times, - }; + selectCell(cellsToSelect, { laneLabels, bucketScore, times }) { + const { selection, swimlaneCellClick, swimlaneData, swimlaneType } = this.props; - const newSelection = { - selectedType: swimlaneType, - selectedLanes: laneLabels, - selectedTimes: d3.extent(times), - }; + let triggerNewSelection = false; - if (_.isEqual(oldSelection, newSelection)) { - triggerNewSelection = false; - } + if (cellsToSelect.length > 1 || bucketScore > 0) { + triggerNewSelection = true; + } - if (triggerNewSelection === false) { - swimlaneCellClick({}); - return; - } + // Check if the same cells were selected again, if so clear the selection, + // otherwise activate the new selection. The two objects are built for + // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" + // since it also includes the "viewBy" attribute which might differ depending + // on whether the overall or viewby swimlane was selected. + const oldSelection = { + selectedType: selection && selection.type, + selectedLanes: selection && selection.lanes, + selectedTimes: selection && selection.times, + }; - const selectedCells = { - viewByFieldName: swimlaneData.fieldName, - lanes: laneLabels, - times: d3.extent(times), - type: swimlaneType, - }; - swimlaneCellClick(selectedCells); + const newSelection = { + selectedType: swimlaneType, + selectedLanes: laneLabels, + selectedTimes: d3.extent(times), + }; + + if (_.isEqual(oldSelection, newSelection)) { + triggerNewSelection = false; } - highlightOverall(times) { - const overallSwimlane = d3.select('.ml-swimlane-overall'); - times.forEach(time => { - const overallCell = overallSwimlane - .selectAll(`div[data-time="${time}"]`) - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect'); - overallCell.classed('sl-cell-inner-selected', true); - }); + if (triggerNewSelection === false) { + swimlaneCellClick({}); + return; } - highlightSelection(cellsToSelect, laneLabels, times) { - const { swimlaneType } = this.props; + const selectedCells = { + viewByFieldName: swimlaneData.fieldName, + lanes: laneLabels, + times: d3.extent(times), + type: swimlaneType, + }; + swimlaneCellClick(selectedCells); + } - // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); + highlightOverall(times) { + const overallSwimlane = d3.select('.ml-swimlane-overall'); + times.forEach(time => { + const overallCell = overallSwimlane + .selectAll(`div[data-time="${time}"]`) + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect'); + overallCell.classed('sl-cell-inner-selected', true); + }); + } - wrapper.selectAll('.lane-label').classed('lane-label-masked', true); - wrapper + highlightSelection(cellsToSelect, laneLabels, times) { + const { swimlaneType } = this.props; + + // This selects both overall and viewby swimlane + const wrapper = d3.selectAll('.ml-explorer-swimlane'); + + wrapper.selectAll('.lane-label').classed('lane-label-masked', true); + wrapper + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') + .classed('sl-cell-inner-masked', true); + wrapper + .selectAll( + '.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected' + ) + .classed('sl-cell-inner-selected', false); + + d3.selectAll(cellsToSelect) + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') + .classed('sl-cell-inner-masked', false) + .classed('sl-cell-inner-selected', true); + + const rootParent = d3.select(this.rootNode.parentNode); + rootParent.selectAll('.lane-label').classed('lane-label-masked', function() { + return laneLabels.indexOf(d3.select(this).text()) === -1; + }); + + if (swimlaneType === 'viewBy') { + // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. + this.highlightOverall(times); + } + } + + maskIrrelevantSwimlanes(maskAll) { + if (maskAll === true) { + // This selects both overall and viewby swimlane + const allSwimlanes = d3.selectAll('.ml-explorer-swimlane'); + allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); + allSwimlanes .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') .classed('sl-cell-inner-masked', true); - wrapper - .selectAll( - '.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected' - ) - .classed('sl-cell-inner-selected', false); - - d3.selectAll(cellsToSelect) + } else { + const overallSwimlane = d3.select('.ml-swimlane-overall'); + overallSwimlane.selectAll('.lane-label').classed('lane-label-masked', true); + overallSwimlane .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', false) - .classed('sl-cell-inner-selected', true); + .classed('sl-cell-inner-masked', true); + } + } - const rootParent = d3.select(this.rootNode.parentNode); - rootParent.selectAll('.lane-label').classed('lane-label-masked', function() { - return laneLabels.indexOf(d3.select(this).text()) === -1; - }); + clearSelection() { + // This selects both overall and viewby swimlane + const wrapper = d3.selectAll('.ml-explorer-swimlane'); + + wrapper.selectAll('.lane-label').classed('lane-label-masked', false); + wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); + wrapper + .selectAll('.sl-cell-inner.sl-cell-inner-selected') + .classed('sl-cell-inner-selected', false); + wrapper + .selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected') + .classed('sl-cell-inner-selected', false); + wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); + } - if (swimlaneType === 'viewBy') { - // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. - this.highlightOverall(times); - } - } + renderSwimlane() { + const element = d3.select(this.rootNode.parentNode); - maskIrrelevantSwimlanes(maskAll) { - if (maskAll === true) { - // This selects both overall and viewby swimlane - const allSwimlanes = d3.selectAll('.ml-explorer-swimlane'); - allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); - allSwimlanes - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } else { - const overallSwimlane = d3.select('.ml-swimlane-overall'); - overallSwimlane.selectAll('.lane-label').classed('lane-label-masked', true); - overallSwimlane - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } + // Consider the setting to support to select a range of cells + if (!ALLOW_CELL_RANGE_SELECTION) { + element.classed(SCSS.mlHideRangeSelection, true); } - clearSelection() { - // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); - - wrapper.selectAll('.lane-label').classed('lane-label-masked', false); - wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); - wrapper - .selectAll('.sl-cell-inner.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper - .selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); + // This getter allows us to fetch the current value in `cellMouseover()`. + // Otherwise it will just refer to the value when `cellMouseover()` was instantiated. + const getCellMouseoverActive = () => this.cellMouseoverActive; + + const { + chartWidth, + filterActive, + maskAll, + TimeBuckets, + swimlaneCellClick, + swimlaneData, + swimlaneType, + selection, + } = this.props; + + const { + laneLabels: lanes, + earliest: startTime, + latest: endTime, + interval: stepSecs, + points, + } = swimlaneData; + + function colorScore(value) { + return getSeverityColor(value); } - renderSwimlane() { - const element = d3.select(this.rootNode.parentNode); + const numBuckets = parseInt((endTime - startTime) / stepSecs); + const cellHeight = 30; + const height = (lanes.length + 1) * cellHeight - 10; + const laneLabelWidth = 170; + + element.style('height', `${height + 20}px`); + const swimlanes = element.select('.ml-swimlanes'); + swimlanes.html(''); + + const cellWidth = Math.floor((chartWidth / numBuckets) * 100) / 100; + + const xAxisWidth = cellWidth * numBuckets; + const xAxisScale = d3.time + .scale() + .domain([new Date(startTime * 1000), new Date(endTime * 1000)]) + .range([0, xAxisWidth]); + + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval(`${stepSecs}s`); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + function cellMouseOverFactory(time, i) { + // Don't use an arrow function here because we need access to `this`, + // which is where d3 supplies a reference to the corresponding DOM element. + return function(lane) { + const bucketScore = getBucketScore(lane, time); + if (bucketScore !== 0) { + cellMouseover(this, lane, bucketScore, i, time); + } + }; + } - // Consider the setting to support to select a range of cells - if (!ALLOW_CELL_RANGE_SELECTION) { - element.classed(SCSS.mlHideRangeSelection, true); + function cellMouseover(target, laneLabel, bucketScore, index, time) { + if (bucketScore === undefined || getCellMouseoverActive() === false) { + return; } - // This getter allows us to fetch the current value in `cellMouseover()`. - // Otherwise it will just refer to the value when `cellMouseover()` was instantiated. - const getCellMouseoverActive = () => this.cellMouseoverActive; - - const { - chartWidth, - filterActive, - maskAll, - TimeBuckets, - swimlaneCellClick, - swimlaneData, - swimlaneType, - selection, - intl, - } = this.props; - - const { - laneLabels: lanes, - earliest: startTime, - latest: endTime, - interval: stepSecs, - points, - } = swimlaneData; - - function colorScore(value) { - return getSeverityColor(value); - } + const displayScore = bucketScore > 1 ? parseInt(bucketScore) : '< 1'; - const numBuckets = parseInt((endTime - startTime) / stepSecs); - const cellHeight = 30; - const height = (lanes.length + 1) * cellHeight - 10; - const laneLabelWidth = 170; - - element.style('height', `${height + 20}px`); - const swimlanes = element.select('.ml-swimlanes'); - swimlanes.html(''); - - const cellWidth = Math.floor((chartWidth / numBuckets) * 100) / 100; - - const xAxisWidth = cellWidth * numBuckets; - const xAxisScale = d3.time - .scale() - .domain([new Date(startTime * 1000), new Date(endTime * 1000)]) - .range([0, xAxisWidth]); - - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval(`${stepSecs}s`); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - function cellMouseOverFactory(time, i) { - // Don't use an arrow function here because we need access to `this`, - // which is where d3 supplies a reference to the corresponding DOM element. - return function(lane) { - const bucketScore = getBucketScore(lane, time); - if (bucketScore !== 0) { - cellMouseover(this, lane, bucketScore, i, time); - } - }; - } + // Display date using same format as Kibana visualizations. + const formattedDate = formatHumanReadableDateTime(time * 1000); + const tooltipData = [{ name: formattedDate }]; - function cellMouseover(target, laneLabel, bucketScore, index, time) { - if (bucketScore === undefined || getCellMouseoverActive() === false) { - return; - } + if (swimlaneData.fieldName !== undefined) { + tooltipData.push({ + name: swimlaneData.fieldName, + value: laneLabel, + seriesKey: laneLabel, + yAccessor: 'fieldName', + }); + } + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { + defaultMessage: 'Max anomaly score', + }), + value: displayScore, + color: colorScore(displayScore), + seriesKey: laneLabel, + yAccessor: 'anomaly_score', + }); - const displayScore = bucketScore > 1 ? parseInt(bucketScore) : '< 1'; + const offsets = target.className === 'sl-cell-inner' ? { x: 6, y: 0 } : { x: 8, y: 1 }; + mlChartTooltipService.show(tooltipData, target, { + x: target.offsetWidth + offsets.x, + y: 6 + offsets.y, + }); + } - // Display date using same format as Kibana visualizations. - const formattedDate = formatHumanReadableDateTime(time * 1000); - const tooltipData = [{ name: formattedDate }]; + function cellMouseleave() { + mlChartTooltipService.hide(); + } - if (swimlaneData.fieldName !== undefined) { - tooltipData.push({ - name: swimlaneData.fieldName, - value: laneLabel, - seriesKey: laneLabel, - yAccessor: 'fieldName', + const d3Lanes = swimlanes.selectAll('.lane').data(lanes); + const d3LanesEnter = d3Lanes + .enter() + .append('div') + .classed('lane', true); + + d3LanesEnter + .append('div') + .classed('lane-label', true) + .style('width', `${laneLabelWidth}px`) + .html(label => { + const showFilterContext = filterActive === true && label === 'Overall'; + if (showFilterContext) { + return i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { + defaultMessage: '{label} (unfiltered)', + values: { label: mlEscape(label) }, }); + } else { + return mlEscape(label); } - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', - defaultMessage: 'Max anomaly score', - }), - value: displayScore, - color: colorScore(displayScore), - seriesKey: laneLabel, - yAccessor: 'anomaly_score', - }); + }) + .on('click', () => { + if (selection && typeof selection.lanes !== 'undefined') { + swimlaneCellClick({}); + } + }) + .each(function() { + if (swimlaneData.fieldName !== undefined) { + d3.select(this) + .on('mouseover', label => { + mlChartTooltipService.show( + [{ skipHeader: true }, { name: swimlaneData.fieldName, value: label }], + this, + { + x: laneLabelWidth, + y: 0, + } + ); + }) + .on('mouseout', () => { + mlChartTooltipService.hide(); + }) + .attr('aria-label', label => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(label)}`); + } + }); - const offsets = target.className === 'sl-cell-inner' ? { x: 6, y: 0 } : { x: 8, y: 1 }; - mlChartTooltipService.show(tooltipData, target, { - x: target.offsetWidth + offsets.x, - y: 6 + offsets.y, - }); - } + const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); - function cellMouseleave() { - mlChartTooltipService.hide(); + function getBucketScore(lane, time) { + let bucketScore = 0; + const point = points.find(p => { + return p.value > 0 && p.laneLabel === lane && p.time === time; + }); + if (typeof point !== 'undefined') { + bucketScore = point.value; } + return bucketScore; + } - const d3Lanes = swimlanes.selectAll('.lane').data(lanes); - const d3LanesEnter = d3Lanes - .enter() - .append('div') - .classed('lane', true); - - d3LanesEnter - .append('div') - .classed('lane-label', true) - .style('width', `${laneLabelWidth}px`) - .html(label => { - const showFilterContext = filterActive === true && label === 'Overall'; - if (showFilterContext) { - return i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { - defaultMessage: '{label} (unfiltered)', - values: { label: mlEscape(label) }, - }); - } else { - return mlEscape(label); - } - }) - .on('click', () => { - if (selection && typeof selection.lanes !== 'undefined') { - swimlaneCellClick({}); - } - }) - .each(function() { - if (swimlaneData.fieldName !== undefined) { - d3.select(this) - .on('mouseover', label => { - mlChartTooltipService.show( - [{ skipHeader: true }, { name: swimlaneData.fieldName, value: label }], - this, - { - x: laneLabelWidth, - y: 0, - } - ); - }) - .on('mouseout', () => { - mlChartTooltipService.hide(); - }) - .attr( - 'aria-label', - label => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(label)}` - ); - } - }); + // TODO - mark if zoomed in to bucket width? + let time = startTime; + Array(numBuckets || 0) + .fill(null) + .forEach((v, i) => { + const cell = cellsContainer + .append('div') + .classed('sl-cell', true) + .style('width', `${cellWidth}px`) + .attr('data-lane-label', label => mlEscape(label)) + .attr('data-time', time) + .attr('data-bucket-score', lane => { + return getBucketScore(lane, time); + }) + // use a factory here to bind the `time` and `i` values + // of this iteration to the event. + .on('mouseover', cellMouseOverFactory(time, i)) + .on('mouseleave', cellMouseleave) + .each(function(laneLabel) { + this.__clickData__ = { + bucketScore: getBucketScore(laneLabel, time), + laneLabel, + swimlaneType, + time, + }; + }); - const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); + // calls itself with each() to get access to lane (= d3 data) + cell.append('div').each(function(lane) { + const el = d3.select(this); - function getBucketScore(lane, time) { - let bucketScore = 0; - const point = points.find(p => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - if (typeof point !== 'undefined') { - bucketScore = point.value; - } - return bucketScore; - } + let color = 'none'; + let bucketScore = 0; - // TODO - mark if zoomed in to bucket width? - let time = startTime; - Array(numBuckets || 0) - .fill(null) - .forEach((v, i) => { - const cell = cellsContainer - .append('div') - .classed('sl-cell', true) - .style('width', `${cellWidth}px`) - .attr('data-lane-label', label => mlEscape(label)) - .attr('data-time', time) - .attr('data-bucket-score', lane => { - return getBucketScore(lane, time); - }) - // use a factory here to bind the `time` and `i` values - // of this iteration to the event. - .on('mouseover', cellMouseOverFactory(time, i)) - .on('mouseleave', cellMouseleave) - .each(function(laneLabel) { - this.__clickData__ = { - bucketScore: getBucketScore(laneLabel, time), - laneLabel, - swimlaneType, - time, - }; - }); - - // calls itself with each() to get access to lane (= d3 data) - cell.append('div').each(function(lane) { - const el = d3.select(this); - - let color = 'none'; - let bucketScore = 0; - - const point = points.find(p => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - - if (typeof point !== 'undefined') { - bucketScore = point.value; - color = colorScore(bucketScore); - el.classed('sl-cell-inner', true).style('background-color', color); - } else { - el.classed('sl-cell-inner-dragselect', true); - } + const point = points.find(p => { + return p.value > 0 && p.laneLabel === lane && p.time === time; }); - time += stepSecs; + if (typeof point !== 'undefined') { + bucketScore = point.value; + color = colorScore(bucketScore); + el.classed('sl-cell-inner', true).style('background-color', color); + } else { + el.classed('sl-cell-inner-dragselect', true); + } }); - // ['x-axis'] is just a placeholder so we have an array of 1. - const laneTimes = swimlanes - .selectAll('.time-tick-labels') - .data(['x-axis']) - .enter() - .append('div') - .classed('time-tick-labels', true); - - // height of .time-tick-labels - const svgHeight = 25; - const svg = laneTimes - .append('svg') - .attr('width', chartWidth) - .attr('height', svgHeight); - - const xAxis = d3.svg - .axis() - .scale(xAxisScale) - .ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) - .tickFormat(tick => moment(tick).format(xAxisTickFormat)); - - const gAxis = svg - .append('g') - .attr('class', 'x axis') - .call(xAxis); - - // remove overlapping labels - let overlapCheck = 0; - gAxis.selectAll('g.tick').each(function() { - const tick = d3.select(this); - const xTransform = d3.transform(tick.attr('transform')).translate[0]; - const tickWidth = tick - .select('text') - .node() - .getBBox().width; - const xMinOffset = xTransform - tickWidth / 2; - const xMaxOffset = xTransform + tickWidth / 2; - // if the tick label overlaps the previous label - // (or overflows the chart to the left), remove it; - // otherwise pick that label's offset as the new offset to check against - if (xMinOffset < overlapCheck) { - tick.remove(); - } else { - overlapCheck = xTransform + tickWidth / 2; - } - // if the last tick label overflows the chart to the right, remove it - if (xMaxOffset > chartWidth) { - tick.remove(); - } + time += stepSecs; }); - // Check for selection and reselect the corresponding swimlane cell - // if the time range and lane label are still in view. - const selectionState = selection; - const selectedType = _.get(selectionState, 'type', undefined); - const selectionViewByFieldName = _.get(selectionState, 'viewByFieldName', ''); - - // If a selection was done in the other swimlane, add the "masked" classes - // to de-emphasize the swimlane cells. - if (swimlaneType !== selectedType && selectedType !== undefined) { - element.selectAll('.lane-label').classed('lane-label-masked', true); - element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); + // ['x-axis'] is just a placeholder so we have an array of 1. + const laneTimes = swimlanes + .selectAll('.time-tick-labels') + .data(['x-axis']) + .enter() + .append('div') + .classed('time-tick-labels', true); + + // height of .time-tick-labels + const svgHeight = 25; + const svg = laneTimes + .append('svg') + .attr('width', chartWidth) + .attr('height', svgHeight); + + const xAxis = d3.svg + .axis() + .scale(xAxisScale) + .ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) + .tickFormat(tick => moment(tick).format(xAxisTickFormat)); + + const gAxis = svg + .append('g') + .attr('class', 'x axis') + .call(xAxis); + + // remove overlapping labels + let overlapCheck = 0; + gAxis.selectAll('g.tick').each(function() { + const tick = d3.select(this); + const xTransform = d3.transform(tick.attr('transform')).translate[0]; + const tickWidth = tick + .select('text') + .node() + .getBBox().width; + const xMinOffset = xTransform - tickWidth / 2; + const xMaxOffset = xTransform + tickWidth / 2; + // if the tick label overlaps the previous label + // (or overflows the chart to the left), remove it; + // otherwise pick that label's offset as the new offset to check against + if (xMinOffset < overlapCheck) { + tick.remove(); + } else { + overlapCheck = xTransform + tickWidth / 2; } + // if the last tick label overflows the chart to the right, remove it + if (xMaxOffset > chartWidth) { + tick.remove(); + } + }); + + // Check for selection and reselect the corresponding swimlane cell + // if the time range and lane label are still in view. + const selectionState = selection; + const selectedType = _.get(selectionState, 'type', undefined); + const selectionViewByFieldName = _.get(selectionState, 'viewByFieldName', ''); + + // If a selection was done in the other swimlane, add the "masked" classes + // to de-emphasize the swimlane cells. + if (swimlaneType !== selectedType && selectedType !== undefined) { + element.selectAll('.lane-label').classed('lane-label-masked', true); + element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); + } - this.props.swimlaneRenderDoneListener(); + this.props.swimlaneRenderDoneListener(); + + if ( + (swimlaneType !== selectedType || + (swimlaneData.fieldName !== undefined && + swimlaneData.fieldName !== selectionViewByFieldName)) && + filterActive === false + ) { + // Not this swimlane which was selected. + return; + } + const cellsToSelect = []; + const selectedLanes = _.get(selectionState, 'lanes', []); + const selectedTimes = _.get(selectionState, 'times', []); + const selectedTimeExtent = d3.extent(selectedTimes); + + selectedLanes.forEach(selectedLane => { if ( - (swimlaneType !== selectedType || - (swimlaneData.fieldName !== undefined && - swimlaneData.fieldName !== selectionViewByFieldName)) && - filterActive === false + lanes.indexOf(selectedLane) > -1 && + selectedTimeExtent[0] >= startTime && + selectedTimeExtent[1] <= endTime ) { - // Not this swimlane which was selected. - return; + // Locate matching cell - look for exact time, otherwise closest before. + const swimlaneElements = element.select('.ml-swimlanes'); + const laneCells = swimlaneElements.selectAll( + `div[data-lane-label="${mlEscape(selectedLane)}"]` + ); + + laneCells.each(function() { + const cell = d3.select(this); + const cellTime = cell.attr('data-time'); + if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { + cellsToSelect.push(cell.node()); + } + }); } + }); - const cellsToSelect = []; - const selectedLanes = _.get(selectionState, 'lanes', []); - const selectedTimes = _.get(selectionState, 'times', []); - const selectedTimeExtent = d3.extent(selectedTimes); + const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { + return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); + }, 0); - selectedLanes.forEach(selectedLane => { - if ( - lanes.indexOf(selectedLane) > -1 && - selectedTimeExtent[0] >= startTime && - selectedTimeExtent[1] <= endTime - ) { - // Locate matching cell - look for exact time, otherwise closest before. - const swimlaneElements = element.select('.ml-swimlanes'); - const laneCells = swimlaneElements.selectAll( - `div[data-lane-label="${mlEscape(selectedLane)}"]` - ); + const selectedCellTimes = cellsToSelect.map(e => { + return d3.select(e).node().__clickData__.time; + }); - laneCells.each(function() { - const cell = d3.select(this); - const cellTime = cell.attr('data-time'); - if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { - cellsToSelect.push(cell.node()); - } - }); - } - }); - - const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { - return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); - }, 0); - - const selectedCellTimes = cellsToSelect.map(e => { - return d3.select(e).node().__clickData__.time; - }); - - if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - this.highlightSelection(cellsToSelect, selectedLanes, selectedCellTimes); - } else if (filterActive === true) { - if (selectedCellTimes.length > 0) { - this.highlightOverall(selectedCellTimes); - } - this.maskIrrelevantSwimlanes(maskAll); - } else { - this.clearSelection(); + if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { + this.highlightSelection(cellsToSelect, selectedLanes, selectedCellTimes); + } else if (filterActive === true) { + if (selectedCellTimes.length > 0) { + this.highlightOverall(selectedCellTimes); } + this.maskIrrelevantSwimlanes(maskAll); + } else { + this.clearSelection(); } + } - shouldComponentUpdate() { - return true; - } + shouldComponentUpdate() { + return true; + } - setRef(componentNode) { - this.rootNode = componentNode; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - render() { - const { swimlaneType } = this.props; + render() { + const { swimlaneType } = this.props; - return ( -
- ); - } + return ( +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js index adc740af12057..20a23bcc7968e 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_swimlane.test.mocks'; import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; import moment from 'moment-timezone'; @@ -14,13 +13,6 @@ import React from 'react'; import { dragSelect$ } from './explorer_dashboard_service'; import { ExplorerSwimlane } from './explorer_swimlane'; -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: jest.fn(), - }), -})); - jest.mock('./explorer_dashboard_service', () => ({ dragSelect$: { subscribe: jest.fn(() => ({ @@ -64,7 +56,7 @@ describe('ExplorerSwimlane', () => { const swimlaneRenderDoneListener = jest.fn(); const wrapper = mountWithIntl( - { const swimlaneRenderDoneListener = jest.fn(); const wrapper = mountWithIntl( - { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 4818856b8a8d2..0b41f789bb571 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -12,10 +12,6 @@ import { chain, each, get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; - -import { npStart } from 'ui/new_platform'; -import { timefilter } from 'ui/timefilter'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, @@ -31,6 +27,7 @@ import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; import { mlResultsService } from '../services/results_service'; import { getBoundsRoundedToInterval, TimeBuckets } from '../util/time_buckets'; +import { getTimefilter, getUiSettings } from '../util/dependency_cache'; import { MAX_CATEGORY_EXAMPLES, @@ -40,8 +37,6 @@ import { } from './explorer_constants'; import { getSwimlaneContainerWidth } from './legacy_utils'; -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. export function createJobs(jobs) { @@ -149,9 +144,9 @@ export function getInfluencers(selectedJobs = []) { } export function getDateFormatTz() { - const config = npStart.core.uiSettings; + const uiSettings = getUiSettings(); // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. - const tzConfig = config.get('dateFormat:tz'); + const tzConfig = uiSettings.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); return dateFormatTz; } @@ -238,6 +233,7 @@ export function getSelectionJobIds(selectedCells, selectedJobs) { export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth) { // Bucketing interval should be the maximum of the chart related interval (i.e. time range related) // and the max bucket span for the jobs shown in the chart. + const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const buckets = new TimeBuckets(); buckets.setInterval('auto'); @@ -544,10 +540,6 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, : selectedJobs.map(d => d.id); const timeRange = getSelectionTimeRange(selectedCells, interval, bounds); - if (mlAnnotationsEnabled === false) { - return Promise.resolve([]); - } - return new Promise(resolve => { ml.annotations .getAnnotations({ @@ -816,6 +808,7 @@ export function loadViewBySwimlane( } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. + const timefilter = getTimefilter(); const timefilterBounds = timefilter.getActiveBounds(); const searchBounds = getBoundsRoundedToInterval( timefilterBounds, diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts index 992357a82efaa..87a9548a432b1 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore import numeral from '@elastic/numeral'; /** diff --git a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index f0539a5f8c9ab..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.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. - */ - -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -uiModules.get('xpack/ml').run(() => { - const showAppLink = xpackInfo.get('features.ml.showLinks', false); - - const navLinkUpdates = { - // hide by default, only show once the xpackInfo is initialized - hidden: !showAppLink, - disabled: !showAppLink || (showAppLink && !xpackInfo.get('features.ml.isAvailable', false)), - }; - - npStart.core.chrome.navLinks.update('ml', navLinkUpdates); -}); 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 index d78efe632501b..4c0956a46d669 100644 --- 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 @@ -16,10 +16,9 @@ import { EuiTextArea, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMlKibana } from '../../../contexts/kibana'; import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; import { getTestUrl } from './utils'; @@ -49,6 +48,9 @@ export interface CustomUrlListProps { * with buttons for testing and deleting each custom URL. */ export const CustomUrlList: FC = ({ job, customUrls, setCustomUrls }) => { + const { + services: { notifications }, + } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); const onLabelChange = (e: ChangeEvent, index: number) => { @@ -106,7 +108,9 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust .catch(resp => { // eslint-disable-next-line no-console console.error('Error obtaining URL for test:', resp); - toastNotifications.addDanger( + + const { toasts } = notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.customUrlEditorList.obtainingUrlToTestConfigurationErrorMessage', { 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 index ef36e84d94d14..cb7c9478244aa 100644 --- 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 @@ -6,7 +6,6 @@ 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'; @@ -16,6 +15,7 @@ import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_ import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; +import { getSavedObjectsClient } from '../../../util/dependency_cache'; export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { // Returns the settings object in the format used by the custom URL editor @@ -133,7 +133,7 @@ function buildDashboardUrlFromSettings(settings) { return new Promise((resolve, reject) => { const { dashboardId, queryFieldNames } = settings.kibanaSettings; - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .get('dashboard', dashboardId) .then(response => { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 35e2e73a880d0..15ccba6316e03 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -19,28 +19,28 @@ import { EuiFlexItem, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { loadFullJob } from '../utils'; import { mlCreateWatchService } from './create_watch_service'; import { CreateWatch } from './create_watch_view'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; -function getSuccessToast(id, url, intl) { +function getSuccessToast(id, url) { return { - title: intl.formatMessage( + title: i18n.translate( + 'xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage', { - id: 'xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage', defaultMessage: 'Watch {id} created successfully', - }, - { id } + values: { id }, + } ), text: ( - {intl.formatMessage({ - id: 'xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', + {i18n.translate('xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', { defaultMessage: 'Edit watch', })} @@ -51,7 +51,7 @@ function getSuccessToast(id, url, intl) { }; } -class CreateWatchFlyoutUI extends Component { +export class CreateWatchFlyoutUI extends Component { constructor(props) { super(props); @@ -100,19 +100,21 @@ class CreateWatchFlyoutUI extends Component { }; save = () => { - const { intl } = this.props; + const { toasts } = this.props.kibana.services.notifications; mlCreateWatchService .createNewWatch(this.state.jobId) .then(resp => { - toastNotifications.addSuccess(getSuccessToast(resp.id, resp.url, intl)); + toasts.addSuccess(getSuccessToast(resp.id, resp.url)); this.closeFlyout(true); }) .catch(error => { - toastNotifications.addDanger( - intl.formatMessage({ - id: 'xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage', - defaultMessage: 'Could not save watch', - }) + toasts.addDanger( + i18n.translate( + 'xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage', + { + defaultMessage: 'Could not save watch', + } + ) ); console.error(error); }); @@ -176,4 +178,4 @@ CreateWatchFlyoutUI.propTypes = { flyoutHidden: PropTypes.func, }; -export const CreateWatchFlyout = injectI18n(CreateWatchFlyoutUI); +export const CreateWatchFlyout = withKibana(CreateWatchFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 5b4a02a7c754f..887afeb3ba818 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { template } from 'lodash'; import { http } from '../../../../services/http_service'; @@ -12,6 +11,7 @@ import emailBody from './email.html'; import emailInfluencersBody from './email_influencers.html'; import { watch } from './watch.js'; import { i18n } from '@kbn/i18n'; +import { getBasePath, getAppUrl } from '../../../../util/dependency_cache'; const compiledEmailBody = template(emailBody); const compiledEmailInfluencersBody = template(emailInfluencersBody); @@ -38,8 +38,9 @@ function randomNumber(min, max) { } function saveWatch(watchModel) { - const basePath = chrome.addBasePath('/api/watcher'); - const url = `${basePath}/watch/${watchModel.id}`; + const basePath = getBasePath(); + const path = basePath.prepend('/api/watcher'); + const url = `${path}/watch/${watchModel.id}`; return http({ url, @@ -95,7 +96,7 @@ class CreateWatchService { // create the html by adding the variables to the compiled email body. emailSection.send_email.email.body.html = compiledEmailBody({ - serverAddress: chrome.getAppUrl(), + serverAddress: getAppUrl(), influencersSection: this.config.includeInfluencers === true ? compiledEmailInfluencersBody({ @@ -156,11 +157,12 @@ class CreateWatchService { }, }; + const basePath = getBasePath(); if (id !== '') { saveWatch(watchModel) .then(() => { this.status.watch = this.STATUS.SAVED; - this.config.watcherEditURL = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/watcher/watches/watch/${id}/edit?_g=()`; + this.config.watcherEditURL = `${basePath.get()}/app/kibana#/management/elasticsearch/watcher/watches/watch/${id}/edit?_g=()`; resolve({ id, url: this.config.watcherEditURL, @@ -180,8 +182,9 @@ class CreateWatchService { loadWatch(jobId) { const id = `ml-${jobId}`; - const basePath = chrome.addBasePath('/api/watcher'); - const url = `${basePath}/watch/${id}`; + const basePath = getBasePath(); + const path = basePath.prepend('/api/watcher'); + const url = `${path}/watch/${id}`; return http({ url, method: 'GET', diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 7a855301885a9..0595ce5caf931 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,7 +17,8 @@ import { EuiCheckbox, EuiFieldText, EuiCallOut } from '@elastic/eui'; import { has } from 'lodash'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ml } from '../../../../services/ml_api_service'; @@ -25,194 +26,195 @@ import { SelectSeverity } from './select_severity'; import { mlCreateWatchService } from './create_watch_service'; const STATUS = mlCreateWatchService.STATUS; -export const CreateWatch = injectI18n( - class CreateWatch extends Component { - static propTypes = { - jobId: PropTypes.string.isRequired, - bucketSpan: PropTypes.string.isRequired, +export class CreateWatch extends Component { + static propTypes = { + jobId: PropTypes.string.isRequired, + bucketSpan: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + mlCreateWatchService.reset(); + this.config = mlCreateWatchService.config; + + this.state = { + jobId: this.props.jobId, + bucketSpan: this.props.bucketSpan, + interval: this.config.interval, + threshold: this.config.threshold, + includeEmail: this.config.emailIncluded, + email: this.config.email, + emailEnabled: false, + status: null, + watchAlreadyExists: false, }; + } - constructor(props) { - super(props); - mlCreateWatchService.reset(); - this.config = mlCreateWatchService.config; - - this.state = { - jobId: this.props.jobId, - bucketSpan: this.props.bucketSpan, - interval: this.config.interval, - threshold: this.config.threshold, - includeEmail: this.config.emailIncluded, - email: this.config.email, - emailEnabled: false, - status: null, - watchAlreadyExists: false, - }; - } - - componentDidMount() { - // make the interval 2 times the bucket span - if (this.state.bucketSpan) { - const intervalObject = parseInterval(this.state.bucketSpan); - let bs = intervalObject.asMinutes() * 2; - if (bs < 1) { - bs = 1; - } - - const interval = `${bs}m`; - this.setState({ interval }, () => { - this.config.interval = interval; - }); + componentDidMount() { + // make the interval 2 times the bucket span + if (this.state.bucketSpan) { + const intervalObject = parseInterval(this.state.bucketSpan); + let bs = intervalObject.asMinutes() * 2; + if (bs < 1) { + bs = 1; } - // load elasticsearch settings to see if email has been configured - ml.getNotificationSettings().then(resp => { - if (has(resp, 'defaults.xpack.notification.email')) { - this.setState({ emailEnabled: true }); - } - }); - - mlCreateWatchService - .loadWatch(this.state.jobId) - .then(() => { - this.setState({ watchAlreadyExists: true }); - }) - .catch(() => { - this.setState({ watchAlreadyExists: false }); - }); - } - - onThresholdChange = threshold => { - this.setState({ threshold }, () => { - this.config.threshold = threshold; - }); - }; - - onIntervalChange = e => { - const interval = e.target.value; + const interval = `${bs}m`; this.setState({ interval }, () => { this.config.interval = interval; }); - }; - - onIncludeEmailChanged = e => { - const includeEmail = e.target.checked; - this.setState({ includeEmail }, () => { - this.config.includeEmail = includeEmail; - }); - }; + } - onEmailChange = e => { - const email = e.target.value; - this.setState({ email }, () => { - this.config.email = email; + // load elasticsearch settings to see if email has been configured + ml.getNotificationSettings().then(resp => { + if (has(resp, 'defaults.xpack.notification.email')) { + this.setState({ emailEnabled: true }); + } + }); + + mlCreateWatchService + .loadWatch(this.state.jobId) + .then(() => { + this.setState({ watchAlreadyExists: true }); + }) + .catch(() => { + this.setState({ watchAlreadyExists: false }); }); - }; - - render() { - const { intl } = this.props; - const { status } = this.state; - - if (status === null || status === STATUS.SAVING || status === STATUS.SAVE_FAILED) { - return ( -
-
-
-
- -
- - ), - }} - /> -
+ } -
-
- -
-
- -
+ onThresholdChange = threshold => { + this.setState({ threshold }, () => { + this.config.threshold = threshold; + }); + }; + + onIntervalChange = e => { + const interval = e.target.value; + this.setState({ interval }, () => { + this.config.interval = interval; + }); + }; + + onIncludeEmailChanged = e => { + const includeEmail = e.target.checked; + this.setState({ includeEmail }, () => { + this.config.includeEmail = includeEmail; + }); + }; + + onEmailChange = e => { + const email = e.target.value; + this.setState({ email }, () => { + this.config.email = email; + }); + }; + + render() { + const { status } = this.state; + + if (status === null || status === STATUS.SAVING || status === STATUS.SAVE_FAILED) { + return ( +
+
+
+
+
-
- {this.state.emailEnabled && ( -
- - } - checked={this.state.includeEmail} - onChange={this.onIncludeEmailChanged} - /> - {this.state.includeEmail && ( -
+ -
- )} + ), + }} + /> +
+ +
+
+ +
+
+
- )} - {this.state.watchAlreadyExists && ( - +
+ {this.state.emailEnabled && ( +
+ } + checked={this.state.includeEmail} + onChange={this.onIncludeEmailChanged} /> - )} -
- ); - } else if (status === STATUS.SAVED) { - return ( -
- + +
+ )} +
+ )} + {this.state.watchAlreadyExists && ( + + } /> -
- ); - } else { - return
; - } + )} +
+ ); + } else if (status === STATUS.SAVED) { + return ( +
+ +
+ ); + } else { + return
; } } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 67398974447f9..3e129a174c9e0 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,160 +17,160 @@ import { import { deleteJobs } from '../utils'; 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 { - static displayName = 'DeleteJobModal'; - static propTypes = { - setShowFunction: PropTypes.func.isRequired, - unsetShowFunction: PropTypes.func.isRequired, - refreshJobs: PropTypes.func.isRequired, +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export class DeleteJobModal extends Component { + static displayName = 'DeleteJobModal'; + static propTypes = { + setShowFunction: PropTypes.func.isRequired, + unsetShowFunction: PropTypes.func.isRequired, + refreshJobs: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + jobs: [], + isModalVisible: false, + deleting: false, }; - constructor(props) { - super(props); - - this.state = { - jobs: [], - isModalVisible: false, - deleting: false, - }; + this.refreshJobs = this.props.refreshJobs; + } - this.refreshJobs = this.props.refreshJobs; + componentDidMount() { + if (typeof this.props.setShowFunction === 'function') { + this.props.setShowFunction(this.showModal); } + } - componentDidMount() { - if (typeof this.props.setShowFunction === 'function') { - this.props.setShowFunction(this.showModal); - } + componentWillUnmount() { + if (typeof this.props.unsetShowFunction === 'function') { + this.props.unsetShowFunction(); } + } - componentWillUnmount() { - if (typeof this.props.unsetShowFunction === 'function') { - this.props.unsetShowFunction(); - } + closeModal = () => { + this.setState({ isModalVisible: false }); + }; + + showModal = jobs => { + this.setState({ + jobs, + isModalVisible: true, + deleting: false, + }); + }; + + deleteJob = () => { + this.setState({ deleting: true }); + deleteJobs(this.state.jobs); + + setTimeout(() => { + this.closeModal(); + this.refreshJobs(); + }, DELETING_JOBS_REFRESH_INTERVAL_MS); + }; + + setEL = el => { + if (el) { + this.el = el; } - - closeModal = () => { - this.setState({ isModalVisible: false }); - }; - - showModal = jobs => { - this.setState({ - jobs, - isModalVisible: true, - deleting: false, - }); - }; - - deleteJob = () => { - this.setState({ deleting: true }); - deleteJobs(this.state.jobs); - - setTimeout(() => { - this.closeModal(); - this.refreshJobs(); - }, DELETING_JOBS_REFRESH_INTERVAL_MS); - }; - - setEL = el => { - if (el) { - this.el = el; - } - }; - - render() { - const { intl } = this.props; - let modal; - - if (this.state.isModalVisible) { - if (this.el && this.state.deleting === true) { - // work around to disable the modal's buttons if the jobs are being deleted - this.el.confirmButton.style.display = 'none'; - this.el.cancelButton.textContent = intl.formatMessage({ - id: 'xpack.ml.jobsList.deleteJobModal.closeButtonLabel', + }; + + render() { + let modal; + + if (this.state.isModalVisible) { + if (this.el && this.state.deleting === true) { + // work around to disable the modal's buttons if the jobs are being deleted + this.el.confirmButton.style.display = 'none'; + this.el.cancelButton.textContent = i18n.translate( + 'xpack.ml.jobsList.deleteJobModal.closeButtonLabel', + { defaultMessage: 'Close', - }); - } - - const title = ( - + } ); - modal = ( - - - } - confirmButtonText={ + } + + const title = ( + + ); + modal = ( + + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + className="eui-textBreakWord" + > + {this.state.deleting === true && ( +
- } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - className="eui-textBreakWord" - > - {this.state.deleting === true && ( -
+ +
+ +
+
+ )} + + {this.state.deleting === false && ( + +

- -

- -
-
- )} - - {this.state.deleting === false && ( - -

- -

-

- -

-
- )} -
-
- ); - } - - return
{modal}
; + values={{ + jobsCount: this.state.jobs.length, + }} + /> +

+ + )} +
+
+ ); } + + return
{modal}
; } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 3e100ed8637ad..7c1639395e02e 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -27,10 +27,11 @@ import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; import { mlMessageBarService } from '../../../../components/messagebar'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -class EditJobFlyoutUI extends Component { +export class EditJobFlyoutUI extends Component { _initialJobFormState = null; constructor(props) { @@ -175,11 +176,13 @@ class EditJobFlyoutUI extends Component { if (jobDetails.jobGroups !== undefined) { if (jobDetails.jobGroups.some(j => this.props.allJobIds.includes(j))) { - jobGroupsValidationError = this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', - defaultMessage: - 'A job with this ID already exists. Groups and jobs cannot use the same ID.', - }); + jobGroupsValidationError = i18n.translate( + 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', + { + defaultMessage: + 'A job with this ID already exists. Groups and jobs cannot use the same ID.', + } + ); } else { jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message; } @@ -229,34 +232,29 @@ class EditJobFlyoutUI extends Component { customUrls: this.state.jobCustomUrls, }; + const { toasts } = this.props.kibana.services.notifications; saveJob(this.state.job, newJobData) .then(() => { - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: 'xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', - defaultMessage: 'Changes to {jobId} saved', - }, - { + toasts.addSuccess( + i18n.translate('xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', { + defaultMessage: 'Changes to {jobId} saved', + values: { jobId: this.state.job.job_id, - } - ) + }, + }) ); this.refreshJobs(); this.closeFlyout(true); }) .catch(error => { console.error(error); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage', - defaultMessage: 'Could not save changes to {jobId}', - }, - { + toasts.addDanger( + i18n.translate('xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage', { + defaultMessage: 'Could not save changes to {jobId}', + values: { jobId: this.state.job.job_id, - } - ) + }, + }) ); mlMessageBarService.notify.error(error); }); @@ -286,13 +284,10 @@ class EditJobFlyoutUI extends Component { isValidJobCustomUrls, } = this.state; - const { intl } = this.props; - const tabs = [ { id: 'job-details', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', { defaultMessage: 'Job details', }), content: ( @@ -308,8 +303,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'detectors', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.detectorsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.detectorsTitle', { defaultMessage: 'Detectors', }), content: ( @@ -322,8 +316,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'datafeed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.datafeedTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.datafeedTitle', { defaultMessage: 'Datafeed', }), content: ( @@ -339,8 +332,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'custom-urls', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.customUrlsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.customUrlsTitle', { defaultMessage: 'Custom URLs', }), content: ( @@ -463,4 +455,4 @@ EditJobFlyoutUI.propTypes = { allJobIds: PropTypes.array.isRequired, }; -export const EditJobFlyout = injectI18n(EditJobFlyoutUI); +export const EditJobFlyout = withKibana(EditJobFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 0c8b7131c3447..a49a2af896be2 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,10 +5,10 @@ */ import { difference } from 'lodash'; -import chrome from 'ui/chrome'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { mlJobService } from '../../../../services/job_service'; import { processCreatedBy } from '../../../../../../common/util/job_utils'; +import { getSavedObjectsClient } from '../../../../util/dependency_cache'; export function saveJob(job, newJobData, finish) { return new Promise((resolve, reject) => { @@ -77,7 +77,7 @@ function saveDatafeed(datafeedData, job) { export function loadSavedDashboards(maxNumber) { // Loads the list of saved dashboards, as used in editing custom URLs. return new Promise((resolve, reject) => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .find({ type: 'dashboard', @@ -109,7 +109,7 @@ export function loadIndexPatterns(maxNumber) { // TODO - amend loadIndexPatterns in index_utils.js to do the request, // without needing an Angular Provider. return new Promise((resolve, reject) => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .find({ type: 'index-pattern', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index c36b4ceed7d57..fe6f72fd10279 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -20,7 +20,6 @@ import { EuiModalHeaderTitle, EuiModalFooter, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -33,11 +32,13 @@ import { getTestUrl, CustomUrlSettings, } from '../../../../components/custom_url_editor/utils'; +import { withKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { loadSavedDashboards, loadIndexPatterns } from '../edit_utils'; import { openCustomUrlWindow } from '../../../../../util/custom_url_utils'; import { Job } from '../../../../new_job/common/job_creator/configs'; import { UrlConfig } from '../../../../../../../common/types/custom_urls'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { MlKibanaReactContextValue } from '../../../../../contexts/kibana'; const MAX_NUMBER_DASHBOARDS = 1000; const MAX_NUMBER_INDEX_PATTERNS = 1000; @@ -47,6 +48,7 @@ interface CustomUrlsProps { jobCustomUrls: UrlConfig[]; setCustomUrls: (customUrls: UrlConfig[]) => void; editMode: 'inline' | 'modal'; + kibana: MlKibanaReactContextValue; } interface CustomUrlsState { @@ -58,7 +60,7 @@ interface CustomUrlsState { editorSettings?: CustomUrlSettings; } -export class CustomUrls extends Component { +class CustomUrlsUI extends Component { constructor(props: CustomUrlsProps) { super(props); @@ -80,6 +82,7 @@ export class CustomUrls extends Component { } componentDidMount() { + const { toasts } = this.props.kibana.services.notifications; loadSavedDashboards(MAX_NUMBER_DASHBOARDS) .then(dashboards => { this.setState({ dashboards }); @@ -87,7 +90,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error loading list of dashboards:', resp); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.loadSavedDashboardsErrorNotificationMessage', { @@ -104,7 +107,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error loading list of dashboards:', resp); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.loadIndexPatternsErrorNotificationMessage', { @@ -143,7 +146,8 @@ export class CustomUrls extends Component { .catch((error: any) => { // eslint-disable-next-line no-console console.error('Error building custom URL from settings:', error); - toastNotifications.addDanger( + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.addNewUrlErrorNotificationMessage', { @@ -156,6 +160,7 @@ export class CustomUrls extends Component { }; onTestButtonClick = () => { + const { toasts } = this.props.kibana.services.notifications; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then(customUrl => { @@ -166,7 +171,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error obtaining URL for test:', resp); - toastNotifications.addWarning( + toasts.addWarning( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.getTestUrlErrorNotificationMessage', { @@ -179,7 +184,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error building custom URL from settings:', resp); - toastNotifications.addWarning( + toasts.addWarning( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.buildUrlErrorNotificationMessage', { @@ -330,3 +335,5 @@ export class CustomUrls extends Component { ); } } + +export const CustomUrls = withKibana(CustomUrlsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index ab2658c0dc124..a609d6a7c3fba 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -10,9 +10,10 @@ import React, { Component } from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiComboBox } from '@elastic/eui'; import { ml } from '../../../../../services/ml_api_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -class JobDetailsUI extends Component { +export class JobDetails extends Component { constructor(props) { super(props); @@ -129,10 +130,12 @@ class JobDetailsUI extends Component { error={groupsValidationError} > ); } -ResultLinksUI.propTypes = { +ResultLinks.propTypes = { jobs: PropTypes.array.isRequired, }; - -export const ResultLinks = injectI18n(ResultLinksUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index e70198b36e0df..41dfdb0dcfeed 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -23,7 +23,8 @@ import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, @@ -35,7 +36,7 @@ const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of forecasts run on an ML job. */ -class ForecastsTableUI extends Component { +export class ForecastsTable extends Component { constructor(props) { super(props); this.state = { @@ -65,10 +66,12 @@ class ForecastsTableUI extends Component { console.log('Error loading list of forecasts for jobs list:', resp); this.setState({ isLoading: false, - errorMessage: this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.loadingErrorMessage', - defaultMessage: 'Error loading the list of forecasts run on this job', - }), + errorMessage: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.loadingErrorMessage', + { + defaultMessage: 'Error loading the list of forecasts run on this job', + } + ), forecasts: [], }); }); @@ -191,13 +194,10 @@ class ForecastsTableUI extends Component { ); } - const { intl } = this.props; - const columns = [ { field: 'forecast_create_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.createdLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.createdLabel', { defaultMessage: 'Created', }), dataType: 'date', @@ -208,8 +208,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_start_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.fromLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.fromLabel', { defaultMessage: 'From', }), dataType: 'date', @@ -219,8 +218,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_end_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.toLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.toLabel', { defaultMessage: 'To', }), dataType: 'date', @@ -230,16 +228,14 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_status', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel', { defaultMessage: 'Status', }), sortable: true, }, { field: 'forecast_memory_bytes', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.memorySizeLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.memorySizeLabel', { defaultMessage: 'Memory size', }), render: bytes => formatNumber(bytes, '0b'), @@ -247,26 +243,21 @@ class ForecastsTableUI extends Component { }, { field: 'processing_time_ms', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel', { defaultMessage: 'Processing time', }), render: ms => - intl.formatMessage( - { - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.msTimeUnitLabel', - defaultMessage: '{ms} ms', - }, - { + i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.msTimeUnitLabel', { + defaultMessage: '{ms} ms', + values: { ms, - } - ), + }, + }), sortable: true, }, { field: 'forecast_expiry_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.expiresLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.expiresLabel', { defaultMessage: 'Expires', }), render: date => formatDate(date, TIME_FORMAT), @@ -275,8 +266,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_messages', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.messagesLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.messagesLabel', { defaultMessage: 'Messages', }), sortable: false, @@ -292,19 +282,18 @@ class ForecastsTableUI extends Component { textOnly: true, }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { defaultMessage: 'View', }), width: '60px', render: forecast => { - const viewForecastAriaLabel = intl.formatMessage( + const viewForecastAriaLabel = i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', { - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', defaultMessage: 'View forecast created at {createdDate}', - }, - { - createdDate: formatDate(forecast.forecast_create_timestamp, TIME_FORMAT), + values: { + createdDate: formatDate(forecast.forecast_create_timestamp, TIME_FORMAT), + }, } ); @@ -333,10 +322,6 @@ class ForecastsTableUI extends Component { ); } } -ForecastsTableUI.propTypes = { +ForecastsTable.propTypes = { job: PropTypes.object.isRequired, }; - -const ForecastsTable = injectI18n(ForecastsTableUI); - -export { ForecastsTable }; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 69891ce0cd2fe..e3f348ad32b0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,12 +17,9 @@ import { AnnotationFlyout } from '../../../../components/annotations/annotation_ import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - -class JobDetailsUI extends Component { +export class JobDetails extends Component { constructor(props) { super(props); @@ -66,14 +63,13 @@ class JobDetailsUI extends Component { datafeedTimingStats, } = extractJobDetails(job); - const { intl, showFullDetails } = this.props; + const { showFullDetails } = this.props; const tabs = [ { id: 'job-settings', 'data-test-subj': 'mlJobListTab-job-settings', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', { defaultMessage: 'Job settings', }), content: ( @@ -87,8 +83,7 @@ class JobDetailsUI extends Component { { id: 'job-config', 'data-test-subj': 'mlJobListTab-job-config', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', { defaultMessage: 'Job config', }), content: ( @@ -101,8 +96,7 @@ class JobDetailsUI extends Component { { id: 'counts', 'data-test-subj': 'mlJobListTab-counts', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.countsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.countsLabel', { defaultMessage: 'Counts', }), content: ( @@ -115,8 +109,7 @@ class JobDetailsUI extends Component { { id: 'json', 'data-test-subj': 'mlJobListTab-json', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jsonLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jsonLabel', { defaultMessage: 'JSON', }), content: , @@ -124,8 +117,7 @@ class JobDetailsUI extends Component { { id: 'job-messages', 'data-test-subj': 'mlJobListTab-job-messages', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', { defaultMessage: 'Job messages', }), content: , @@ -137,8 +129,7 @@ class JobDetailsUI extends Component { tabs.splice(2, 0, { id: 'datafeed', 'data-test-subj': 'mlJobListTab-datafeed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { defaultMessage: 'Datafeed', }), content: ( @@ -153,8 +144,7 @@ class JobDetailsUI extends Component { { id: 'datafeed-preview', 'data-test-subj': 'mlJobListTab-datafeed-preview', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', { defaultMessage: 'Datafeed preview', }), content: , @@ -162,8 +152,7 @@ class JobDetailsUI extends Component { { id: 'forecasts', 'data-test-subj': 'mlJobListTab-forecasts', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', { defaultMessage: 'Forecasts', }), content: , @@ -171,12 +160,11 @@ class JobDetailsUI extends Component { ); } - if (mlAnnotationsEnabled && showFullDetails) { + if (showFullDetails) { tabs.push({ id: 'annotations', 'data-test-subj': 'mlJobListTab-annotations', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', { defaultMessage: 'Annotations', }), content: ( @@ -196,12 +184,10 @@ class JobDetailsUI extends Component { } } } -JobDetailsUI.propTypes = { +JobDetails.propTypes = { jobId: PropTypes.string.isRequired, job: PropTypes.object, addYourself: PropTypes.func.isRequired, removeYourself: PropTypes.func.isRequired, showFullDetails: PropTypes.bool, }; - -export const JobDetails = injectI18n(JobDetailsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 1ad0e2851dedc..a91df3cce01f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -12,8 +12,8 @@ import { JobGroup } from '../job_group'; import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; function loadGroups() { return ml.jobs @@ -42,7 +42,7 @@ function loadGroups() { }); } -class JobFilterBarUI extends Component { +export class JobFilterBar extends Component { constructor(props) { super(props); @@ -87,7 +87,6 @@ class JobFilterBarUI extends Component { }; render() { - const { intl } = this.props; const { error, selectedId } = this.state; const filters = [ { @@ -96,22 +95,19 @@ class JobFilterBarUI extends Component { items: [ { value: 'opened', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.openedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { defaultMessage: 'Opened', }), }, { value: 'closed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.closedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { defaultMessage: 'Closed', }), }, { value: 'failed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.failedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { defaultMessage: 'Failed', }), }, @@ -123,15 +119,13 @@ class JobFilterBarUI extends Component { items: [ { value: 'started', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.startedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { defaultMessage: 'Started', }), }, { value: 'stopped', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.stoppedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { defaultMessage: 'Stopped', }), }, @@ -140,8 +134,7 @@ class JobFilterBarUI extends Component { { type: 'field_value_selection', field: 'groups', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.groupLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { defaultMessage: 'Group', }), multiSelect: 'or', @@ -188,7 +181,7 @@ class JobFilterBarUI extends Component { ); } } -JobFilterBarUI.propTypes = { +JobFilterBar.propTypes = { setFilters: PropTypes.func.isRequired, }; @@ -202,5 +195,3 @@ function getError(error) { return ''; } - -export const JobFilterBar = injectI18n(JobFilterBarUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index b691bc34295c5..7036b4f64b3c5 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,15 +17,15 @@ import { JobIcon } from '../../../../components/job_message_icon'; import { getJobIdUrl } from '../utils'; import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page -class JobsListUI extends Component { +export class JobsList extends Component { constructor(props) { super(props); @@ -99,7 +99,7 @@ class JobsListUI extends Component { } render() { - const { intl, loading, isManagementTable } = this.props; + const { loading, isManagementTable } = this.props; const selectionControls = { selectable: job => job.deleting !== true, selectableMessage: (selectable, rowItem) => @@ -141,20 +141,14 @@ class JobsListUI extends Component { iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'} aria-label={ this.state.itemIdToExpandedRowMap[item.id] - ? intl.formatMessage( - { - id: 'xpack.ml.jobsList.collapseJobDetailsAriaLabel', - defaultMessage: 'Hide details for {itemId}', - }, - { itemId: item.id } - ) - : intl.formatMessage( - { - id: 'xpack.ml.jobsList.expandJobDetailsAriaLabel', - defaultMessage: 'Show details for {itemId}', - }, - { itemId: item.id } - ) + ? i18n.translate('xpack.ml.jobsList.collapseJobDetailsAriaLabel', { + defaultMessage: 'Hide details for {itemId}', + values: { itemId: item.id }, + }) + : i18n.translate('xpack.ml.jobsList.expandJobDetailsAriaLabel', { + defaultMessage: 'Show details for {itemId}', + values: { itemId: item.id }, + }) } data-row-id={item.id} data-test-subj="mlJobListRowDetailsToggle" @@ -165,8 +159,7 @@ class JobsListUI extends Component { { field: 'id', 'data-test-subj': 'mlJobListColumnId', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.idLabel', + name: i18n.translate('xpack.ml.jobsList.idLabel', { defaultMessage: 'ID', }), sortable: true, @@ -190,8 +183,7 @@ class JobsListUI extends Component { render: item => , }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.descriptionLabel', + name: i18n.translate('xpack.ml.jobsList.descriptionLabel', { defaultMessage: 'Description', }), sortable: true, @@ -204,8 +196,7 @@ class JobsListUI extends Component { { field: 'processed_record_count', 'data-test-subj': 'mlJobListColumnRecordCount', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.processedRecordsLabel', + name: i18n.translate('xpack.ml.jobsList.processedRecordsLabel', { defaultMessage: 'Processed records', }), sortable: true, @@ -217,8 +208,7 @@ class JobsListUI extends Component { { field: 'memory_status', 'data-test-subj': 'mlJobListColumnMemoryStatus', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.memoryStatusLabel', + name: i18n.translate('xpack.ml.jobsList.memoryStatusLabel', { defaultMessage: 'Memory status', }), sortable: true, @@ -228,8 +218,7 @@ class JobsListUI extends Component { { field: 'jobState', 'data-test-subj': 'mlJobListColumnJobState', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobStateLabel', + name: i18n.translate('xpack.ml.jobsList.jobStateLabel', { defaultMessage: 'Job state', }), sortable: true, @@ -239,8 +228,7 @@ class JobsListUI extends Component { { field: 'datafeedState', 'data-test-subj': 'mlJobListColumnDatafeedState', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.datafeedStateLabel', + name: i18n.translate('xpack.ml.jobsList.datafeedStateLabel', { defaultMessage: 'Datafeed state', }), sortable: true, @@ -248,8 +236,7 @@ class JobsListUI extends Component { width: '8%', }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.actionsLabel', + name: i18n.translate('xpack.ml.jobsList.actionsLabel', { defaultMessage: 'Actions', }), render: item => , @@ -259,8 +246,7 @@ class JobsListUI extends Component { if (isManagementTable === true) { // insert before last column columns.splice(columns.length - 1, 0, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.spacesLabel', + name: i18n.translate('xpack.ml.jobsList.spacesLabel', { defaultMessage: 'Spaces', }), render: () => {'all'}, @@ -272,8 +258,7 @@ class JobsListUI extends Component { } else { // insert before last column columns.splice(columns.length - 1, 0, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.latestTimestampLabel', + name: i18n.translate('xpack.ml.jobsList.latestTimestampLabel', { defaultMessage: 'Latest timestamp', }), truncateText: false, @@ -341,12 +326,10 @@ class JobsListUI extends Component { loading={loading === true} noItemsMessage={ loading - ? intl.formatMessage({ - id: 'xpack.ml.jobsList.loadingJobsLabel', + ? i18n.translate('xpack.ml.jobsList.loadingJobsLabel', { defaultMessage: 'Loading jobs…', }) - : intl.formatMessage({ - id: 'xpack.ml.jobsList.noJobsFoundLabel', + : i18n.translate('xpack.ml.jobsList.noJobsFoundLabel', { defaultMessage: 'No jobs found', }) } @@ -368,7 +351,7 @@ class JobsListUI extends Component { ); } } -JobsListUI.propTypes = { +JobsList.propTypes = { jobsSummaryList: PropTypes.array.isRequired, fullJobsList: PropTypes.object.isRequired, isManagementTable: PropTypes.bool, @@ -383,10 +366,8 @@ JobsListUI.propTypes = { selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, }; -JobsListUI.defaultProps = { +JobsList.defaultProps = { isManagementTable: false, isMlEnabledInSpace: true, loading: false, }; - -export const JobsList = injectI18n(JobsListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 7d3a9bb878cc1..a5509c0f79a36 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -12,7 +12,8 @@ import React, { Component } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; import { closeJobs, stopDatafeeds, isStartable, isStoppable, isClosable } from '../utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; class MultiJobActionsMenuUI extends Component { constructor(props) { @@ -46,10 +47,12 @@ class MultiJobActionsMenuUI extends Component { size="s" onClick={this.onButtonClick} iconType="gear" - aria-label={this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.multiJobActionsMenu.managementActionsAriaLabel', - defaultMessage: 'Management actions', - })} + aria-label={i18n.translate( + 'xpack.ml.jobsList.multiJobActionsMenu.managementActionsAriaLabel', + { + defaultMessage: 'Management actions', + } + )} color="text" disabled={ anyJobsDeleting || (this.canDeleteJob === false && this.canStartStopDatafeed === false) @@ -155,4 +158,4 @@ MultiJobActionsMenuUI.propTypes = { refreshJobs: PropTypes.func.isRequired, }; -export const MultiJobActionsMenu = injectI18n(MultiJobActionsMenuUI); +export const MultiJobActionsMenu = MultiJobActionsMenuUI; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 8c49f60d058f8..5f91ba9b6f107 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../../privilege/check_privilege'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -23,10 +22,12 @@ import { import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; +import { checkPermission } from '../../../../../privilege/check_privilege'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; import { mlMessageBarService } from '../../../../../components/messagebar'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; function createSelectedGroups(jobs, groups) { const jobIds = jobs.map(j => j.id); @@ -52,220 +53,219 @@ function createSelectedGroups(jobs, groups) { return selectedGroups; } -export const GroupSelector = injectI18n( - class GroupSelector extends Component { - static propTypes = { - jobs: PropTypes.array.isRequired, - allJobIds: PropTypes.array.isRequired, - refreshJobs: PropTypes.func.isRequired, - }; +export class GroupSelector extends Component { + static propTypes = { + jobs: PropTypes.array.isRequired, + allJobIds: PropTypes.array.isRequired, + refreshJobs: PropTypes.func.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - isPopoverOpen: false, - groups: [], - selectedGroups: {}, - edited: false, - }; + this.state = { + isPopoverOpen: false, + groups: [], + selectedGroups: {}, + edited: false, + }; - this.refreshJobs = this.props.refreshJobs; - this.canUpdateJob = checkPermission('canUpdateJob'); - } + this.refreshJobs = this.props.refreshJobs; + this.canUpdateJob = checkPermission('canUpdateJob'); + } - static getDerivedStateFromProps(props, state) { - if (state.edited === false) { - const selectedGroups = createSelectedGroups(props.jobs, state.groups); - return { selectedGroups }; - } else { - return {}; - } + static getDerivedStateFromProps(props, state) { + if (state.edited === false) { + const selectedGroups = createSelectedGroups(props.jobs, state.groups); + return { selectedGroups }; + } else { + return {}; } + } - togglePopover = () => { - if (this.state.isPopoverOpen) { - this.closePopover(); - } else { - ml.jobs - .groups() - .then(groups => { - const selectedGroups = createSelectedGroups(this.props.jobs, groups); + togglePopover = () => { + if (this.state.isPopoverOpen) { + this.closePopover(); + } else { + ml.jobs + .groups() + .then(groups => { + const selectedGroups = createSelectedGroups(this.props.jobs, groups); - this.setState({ - isPopoverOpen: true, - edited: false, - selectedGroups, - groups, - }); - }) - .catch(error => { - console.error(error); + this.setState({ + isPopoverOpen: true, + edited: false, + selectedGroups, + groups, }); - } - }; + }) + .catch(error => { + console.error(error); + }); + } + }; - closePopover = () => { - this.setState({ - edited: false, - isPopoverOpen: false, - }); - }; + closePopover = () => { + this.setState({ + edited: false, + isPopoverOpen: false, + }); + }; - selectGroup = group => { - const newSelectedGroups = cloneDeep(this.state.selectedGroups); + selectGroup = group => { + const newSelectedGroups = cloneDeep(this.state.selectedGroups); - if (newSelectedGroups[group.id] === undefined) { - newSelectedGroups[group.id] = { - partial: false, - }; - } else if (newSelectedGroups[group.id].partial === true) { - newSelectedGroups[group.id].partial = false; - } else { - delete newSelectedGroups[group.id]; - } + if (newSelectedGroups[group.id] === undefined) { + newSelectedGroups[group.id] = { + partial: false, + }; + } else if (newSelectedGroups[group.id].partial === true) { + newSelectedGroups[group.id].partial = false; + } else { + delete newSelectedGroups[group.id]; + } - this.setState({ - selectedGroups: newSelectedGroups, - edited: true, - }); - }; + this.setState({ + selectedGroups: newSelectedGroups, + edited: true, + }); + }; - applyChanges = () => { - const { selectedGroups } = this.state; - const { jobs } = this.props; - const newJobs = jobs.map(j => ({ - id: j.id, - oldGroups: j.groups, - newGroups: [], - })); + applyChanges = () => { + const { selectedGroups } = this.state; + const { jobs } = this.props; + const newJobs = jobs.map(j => ({ + id: j.id, + oldGroups: j.groups, + newGroups: [], + })); - for (const gId in selectedGroups) { - if (selectedGroups.hasOwnProperty(gId)) { - const group = selectedGroups[gId]; - newJobs.forEach(j => { - if (group.partial === false || (group.partial === true && j.oldGroups.includes(gId))) { - j.newGroups.push(gId); - } - }); - } + for (const gId in selectedGroups) { + if (selectedGroups.hasOwnProperty(gId)) { + const group = selectedGroups[gId]; + newJobs.forEach(j => { + if (group.partial === false || (group.partial === true && j.oldGroups.includes(gId))) { + j.newGroups.push(gId); + } + }); } + } - const tempJobs = newJobs.map(j => ({ job_id: j.id, groups: j.newGroups })); - ml.jobs - .updateGroups(tempJobs) - .then(resp => { - let success = true; - for (const jobId in resp) { - // check success of each job update - if (resp.hasOwnProperty(jobId)) { - if (resp[jobId].success === false) { - mlMessageBarService.notify.error(resp[jobId].error); - success = false; - } + const tempJobs = newJobs.map(j => ({ job_id: j.id, groups: j.newGroups })); + ml.jobs + .updateGroups(tempJobs) + .then(resp => { + let success = true; + for (const jobId in resp) { + // check success of each job update + if (resp.hasOwnProperty(jobId)) { + if (resp[jobId].success === false) { + mlMessageBarService.notify.error(resp[jobId].error); + success = false; } } + } - if (success) { - // if all are successful refresh the job list - this.refreshJobs(); - this.closePopover(); - } else { - console.error(resp); - } - }) - .catch(error => { - mlMessageBarService.notify.error(error); - console.error(error); - }); + if (success) { + // if all are successful refresh the job list + this.refreshJobs(); + this.closePopover(); + } else { + console.error(resp); + } + }) + .catch(error => { + mlMessageBarService.notify.error(error); + console.error(error); + }); + }; + + addNewGroup = id => { + const newGroup = { + id, + calendarIds: [], + jobIds: [], }; - addNewGroup = id => { - const newGroup = { - id, - calendarIds: [], - jobIds: [], - }; + const groups = this.state.groups; + if (groups.some(g => g.id === newGroup.id) === false) { + groups.push(newGroup); + } - const groups = this.state.groups; - if (groups.some(g => g.id === newGroup.id) === false) { - groups.push(newGroup); - } + this.setState({ + groups, + }); + }; - this.setState({ - groups, - }); - }; + render() { + const { groups, selectedGroups, edited } = this.state; + const button = ( + + } + > + this.togglePopover()} + disabled={this.canUpdateJob === false} + /> + + ); - render() { - const { intl } = this.props; - const { groups, selectedGroups, edited } = this.state; - const button = ( - this.closePopover()} + > +
+ - } - > - this.togglePopover()} - disabled={this.canUpdateJob === false} - /> - - ); + - return ( - this.closePopover()} - > -
- - - - - + - - + + - + - -
- - - - - - - -
+ +
+ + + + + + +
- - ); - } +
+
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 291e7d4945197..f92f9c2fa4a3d 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -16,108 +16,110 @@ import { keyCodes, } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { validateGroupNames } from '../../../validate_job'; -export const NewGroupInput = injectI18n( - class NewGroupInput extends Component { - static propTypes = { - addNewGroup: PropTypes.func.isRequired, - allJobIds: PropTypes.array.isRequired, - }; +export class NewGroupInput extends Component { + static propTypes = { + addNewGroup: PropTypes.func.isRequired, + allJobIds: PropTypes.array.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - tempNewGroupName: '', - groupsValidationError: '', - }; - } + this.state = { + tempNewGroupName: '', + groupsValidationError: '', + }; + } - changeTempNewGroup = e => { - const tempNewGroupName = e.target.value; - let groupsValidationError = ''; + changeTempNewGroup = e => { + const tempNewGroupName = e.target.value; + let groupsValidationError = ''; - if (tempNewGroupName === '') { - groupsValidationError = ''; - } else if (this.props.allJobIds.includes(tempNewGroupName)) { - groupsValidationError = this.props.intl.formatMessage({ - id: - 'xpack.ml.jobsList.multiJobActions.groupSelector.groupsAndJobsCanNotUseSameIdErrorMessage', + if (tempNewGroupName === '') { + groupsValidationError = ''; + } else if (this.props.allJobIds.includes(tempNewGroupName)) { + groupsValidationError = i18n.translate( + 'xpack.ml.jobsList.multiJobActions.groupSelector.groupsAndJobsCanNotUseSameIdErrorMessage', + { defaultMessage: 'A job with this ID already exists. Groups and jobs cannot use the same ID.', - }); - } else { - groupsValidationError = validateGroupNames([tempNewGroupName]).message; - } + } + ); + } else { + groupsValidationError = validateGroupNames([tempNewGroupName]).message; + } - this.setState({ - tempNewGroupName, - groupsValidationError, - }); - }; + this.setState({ + tempNewGroupName, + groupsValidationError, + }); + }; - newGroupKeyPress = e => { - if ( - e.keyCode === keyCodes.ENTER && - this.state.groupsValidationError === '' && - this.state.tempNewGroupName !== '' - ) { - this.addNewGroup(); - } - }; + newGroupKeyPress = e => { + if ( + e.keyCode === keyCodes.ENTER && + this.state.groupsValidationError === '' && + this.state.tempNewGroupName !== '' + ) { + this.addNewGroup(); + } + }; - addNewGroup = () => { - this.props.addNewGroup(this.state.tempNewGroupName); - this.setState({ tempNewGroupName: '' }); - }; + addNewGroup = () => { + this.props.addNewGroup(this.state.tempNewGroupName); + this.setState({ tempNewGroupName: '' }); + }; - render() { - const { intl } = this.props; - const { tempNewGroupName, groupsValidationError } = this.state; + render() { + const { tempNewGroupName, groupsValidationError } = this.state; - return ( -
- - - + + + + - - - - - - + + + + + - - - -
- ); - } + } + )} + disabled={tempNewGroupName === '' || groupsValidationError !== ''} + /> + + + +
+ ); } -); +} 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 index 57953d99a9f20..2739f32aa1055 100644 --- 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 @@ -5,13 +5,12 @@ */ 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 { getToastNotifications, getBasePath } from '../../../util/dependency_cache'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { i18n } from '@kbn/i18n'; @@ -58,6 +57,7 @@ export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { defaultMessage: 'Jobs failed to start', @@ -78,6 +78,7 @@ export function stopDatafeeds(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { defaultMessage: 'Jobs failed to stop', @@ -139,6 +140,7 @@ function showResults(resp, action) { }); } + const toastNotifications = getToastNotifications(); toastNotifications.addSuccess( i18n.translate('xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage', { defaultMessage: @@ -213,6 +215,7 @@ export async function cloneJob(jobId) { window.location.href = '#/jobs/new_job'; } catch (error) { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { defaultMessage: 'Could not clone {jobId}. Job could not be found', @@ -232,6 +235,7 @@ export function closeJobs(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { defaultMessage: 'Jobs failed to close', @@ -252,6 +256,7 @@ export function deleteJobs(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { defaultMessage: 'Jobs failed to delete', @@ -367,8 +372,9 @@ export function getJobIdUrl(jobId) { }; const encoded = rison.encode(settings); const url = `?mlManagement=${encoded}`; + const basePath = getBasePath(); - return `${chrome.getBasePath()}/app/ml#/jobs${url}`; + return `${basePath.get()}/app/ml#/jobs${url}`; } function getUrlVars(url) { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 8c648696a9a7a..212c5ad6ebb31 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -8,7 +8,7 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; const WIDTH = '512px'; @@ -23,8 +23,8 @@ interface Props { } export const TimeRangePicker: FC = ({ setTimeRange, timeRange }) => { - const kibanaContext = useKibanaContext(); - const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); + const mlContext = useMlContext(); + const dateFormat: string = mlContext.kibanaConfig.get('dateFormat'); const [startMoment, setStartMoment] = useState(moment(timeRange.start)); const [endMoment, setEndMoment] = useState(moment(timeRange.end)); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index c0b9a4872c3c4..e35f3056ce434 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -15,6 +15,10 @@ export interface Datafeed { chunking_config?: ChunkingConfig; frequency?: string; indices: IndexPatternTitle[]; + /** + * The datafeed can contain indexes and indices + */ + indexes?: IndexPatternTitle[]; job_id?: JobId; query: object; query_delay?: string; diff --git a/x-pack/legacy/plugins/ml/public/application/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 index ed4f7729ccb26..3070fc0afdc33 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import { LineSeries, ScaleType, CurveType } from '@elastic/charts'; -import { seriesStyle, LINE_COLOR } from '../common/settings'; +import { seriesStyle, useChartColors } from '../common/settings'; interface Props { chartData: any[]; @@ -19,6 +19,7 @@ const lineSeriesStyle = { }; export const Line: FC = ({ chartData }) => { + const { LINE_COLOR } = useChartColors(); return ( = ({ modelData }) => { + const { MODEL_COLOR } = useChartColors(); const model = modelData === undefined ? [] : modelData; return ( = ({ chartData }) => { + const { LINE_COLOR } = useChartColors(); return ( = ({ loading = false, fadeChart, }) => { + const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; return ( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 3b49ee0f8c11f..76a716fcbec26 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -8,11 +8,14 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; - -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-calendars.html`; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; export const Description: FC = memo(({ children }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`; const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.additionalSection.calendarsSelection.title', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx index 0a667ab9c5b9c..40974418b09b1 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx @@ -8,11 +8,14 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; - -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-configuring-url.html`; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; export const Description: FC = memo(({ children }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`; const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.additionalSection.customUrls.title', { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4a1626ffcef89..5064ba9df9bee 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -14,7 +14,7 @@ import { isAdvancedJobCreator, } from '../../../../../common/job_creator'; import { ml, BucketSpanEstimatorData } from '../../../../../../../services/ml_api_service'; -import { useKibanaContext } from '../../../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../../../contexts/ml'; import { mlMessageBarService } from '../../../../../../../components/messagebar'; export enum ESTIMATE_STATUS { @@ -24,7 +24,7 @@ export enum ESTIMATE_STATUS { export function useEstimateBucketSpan() { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [status, setStatus] = useState(ESTIMATE_STATUS.NOT_RUNNING); @@ -35,10 +35,10 @@ export function useEstimateBucketSpan() { end: jobCreator.end, }, fields: jobCreator.fields.map(f => (f.id === EVENT_RATE_FIELD_ID ? null : f.id)), - index: kibanaContext.currentIndexPattern.title, - query: kibanaContext.combinedQuery, + index: mlContext.currentIndexPattern.title, + query: mlContext.combinedQuery, splitField: undefined, - timeField: kibanaContext.currentIndexPattern.timeFieldName, + timeField: mlContext.currentIndexPattern.timeFieldName, }; if ( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index ebe113a1f8bef..82524b84d9849 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -17,12 +17,12 @@ import { } from '../../../../../common/job_creator'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, falseLabel, trueLabel, defaultLabel, Italic } from '../common'; -import { useKibanaContext } from '../../../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../../../contexts/ml'; export const JobDetails: FC = () => { const { jobCreator } = useContext(JobCreatorContext); - const kibanaContext = useKibanaContext(); - const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); + const mlContext = useMlContext(); + const dateFormat: string = mlContext.kibanaConfig.get('dateFormat'); const { anomaly_detectors: anomalyDetectors } = getNewJobDefaults(); const isAdvanced = isAdvancedJobCreator(jobCreator); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index de019cbe86f9d..c24c018f50d75 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -5,11 +5,11 @@ */ import React, { FC, Fragment, useContext, useState } from 'react'; -import { toastNotifications } from 'ui/notify'; import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; // @ts-ignore import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; @@ -23,6 +23,9 @@ interface Props { type ShowFlyout = (jobId: string) => void; export const PostSaveOptions: FC = ({ jobRunner }) => { + const { + services: { notifications }, + } = useMlKibana(); const { jobCreator } = useContext(JobCreatorContext); const [datafeedState, setDatafeedState] = useState(DATAFEED_STATE.STOPPED); const [watchFlyoutVisible, setWatchFlyoutVisible] = useState(false); @@ -42,12 +45,13 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { } async function startJobInRealTime() { + const { toasts } = notifications; setDatafeedState(DATAFEED_STATE.STARTING); if (jobRunner !== null) { try { const started = await jobRunner.startDatafeedInRealTime(true); setDatafeedState(started === true ? DATAFEED_STATE.STARTED : DATAFEED_STATE.STOPPED); - toastNotifications.addSuccess({ + toasts.addSuccess({ title: i18n.translate( 'xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess', { @@ -58,7 +62,7 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { }); } catch (error) { setDatafeedState(DATAFEED_STATE.STOPPED); - toastNotifications.addDanger({ + toasts.addDanger({ title: i18n.translate( 'xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError', { diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 994847864d6bb..75994b5358899 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -38,6 +38,9 @@ import { import { JobSectionTitle, DatafeedSectionTitle } from './components/common'; export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const { + services: { notifications }, + } = useMlKibana(); const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -67,7 +70,8 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => setJobRunner(jr); } catch (error) { // catch and display all job creation errors - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), @@ -85,7 +89,8 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => advancedStartDatafeed(jobCreator); } catch (error) { // catch and display all job creation errors - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 70a529b8e24d0..f0c5c3ba272c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -6,24 +6,24 @@ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { timefilter } from 'ui/timefilter'; import moment from 'moment'; import { WizardNav } from '../wizard_nav'; import { StepProps, WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; import { TimeRangePicker, TimeRange } from '../../../common/components'; +import { useMlKibana } from '../../../../../contexts/kibana'; export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) => { - const kibanaContext = useKibanaContext(); + const { services } = useMlKibana(); + const mlContext = useMlContext(); const { jobCreator, @@ -63,6 +63,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) max: moment(end), }); // update the timefilter, to keep the URL in sync + const { timefilter } = services.data.query.timefilter; timefilter.setTime({ from: moment(start).toISOString(), to: moment(end).toISOString(), @@ -86,7 +87,8 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) end: range.end.epoch, }); } else { - toastNotifications.addDanger( + const { toasts } = services.notifications; + toasts.addDanger( i18n.translate('xpack.ml.newJob.wizard.timeRangeStep.fullTimeRangeError', { defaultMessage: 'An error occurred obtaining the time range for the index', }) @@ -104,8 +106,8 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 2fbedc1cd39bb..9bb9376f3ea14 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -15,8 +15,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { npStart } from 'ui/new_platform'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; +import { useMlKibana } from '../../../../contexts/kibana'; export interface PageProps { nextStepPath: string; @@ -24,6 +24,7 @@ export interface PageProps { export const Page: FC = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; + const { uiSettings, savedObjects } = useMlKibana().services; const onObjectSelection = (id: string, type: string) => { window.location.href = `${nextStepPath}?${ @@ -77,8 +78,8 @@ export const Page: FC = ({ nextStepPath }) => { }, ]} fixedPageSize={RESULTS_PER_PAGE} - uiSettings={npStart.core.uiSettings} - savedObjects={npStart.core.savedObjects} + uiSettings={uiSettings} + savedObjects={savedObjects} /> diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index b1382aef86d30..562ef780bd17b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,7 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; @@ -27,10 +27,10 @@ import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); - const { currentSavedSearch, currentIndexPattern } = kibanaContext; + const { currentSavedSearch, currentIndexPattern } = mlContext; const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); const indexWarningTitle = diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index bc269b22df880..b2383b6c08a58 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -14,12 +14,12 @@ import { EuiTitle, EuiPageContentBody, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Wizard } from './wizard'; import { WIZARD_STEPS } from '../components/step_types'; import { getJobCreatorTitle } from '../../common/job_creator/util/general'; +import { useMlKibana } from '../../../../contexts/kibana'; import { jobCreatorFactory, isAdvancedJobCreator, @@ -33,7 +33,7 @@ import { import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; import { TimeBuckets } from '../../../../util/time_buckets'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; @@ -52,11 +52,14 @@ export interface PageProps { } export const Page: FC = ({ existingJobsAndGroups, jobType }) => { - const kibanaContext = useKibanaContext(); + const { + services: { notifications }, + } = useMlKibana(); + const mlContext = useMlContext(); const jobCreator = jobCreatorFactory(jobType)( - kibanaContext.currentIndexPattern, - kibanaContext.currentSavedSearch, - kibanaContext.combinedQuery + mlContext.currentIndexPattern, + mlContext.currentSavedSearch, + mlContext.combinedQuery ); const { from, to } = getTimeFilterRange(); @@ -124,7 +127,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { jobCreator.modelPlot = true; } - if (kibanaContext.currentSavedSearch !== null) { + if (mlContext.currentSavedSearch !== null) { // Jobs created from saved searches cannot be cloned in the wizard as the // ML job config holds no reference to the saved search ID. jobCreator.createdBy = null; @@ -147,7 +150,8 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { try { jobCreator.autoSetTimeRange(); } catch (error) { - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error', { defaultMessage: `Error retrieving beginning and end times of index`, }), @@ -175,10 +179,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { chartInterval.setMaxBars(MAX_BARS); chartInterval.setInterval('auto'); - const chartLoader = new ChartLoader( - kibanaContext.currentIndexPattern, - kibanaContext.combinedQuery - ); + const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery); const jobValidator = new JobValidator(jobCreator, existingJobsAndGroups); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index cd3d887c906af..56a787d0d7054 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -19,7 +19,7 @@ import { JobDetailsStep } from '../components/job_details_step'; import { ValidationStep } from '../components/validation_step'; import { SummaryStep } from '../components/summary_step'; import { DatafeedStep } from '../components/datafeed_step'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; interface Props { currentStep: WIZARD_STEPS; @@ -27,24 +27,24 @@ interface Props { } export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); // store whether the advanced and additional sections have been expanded. // has to be stored at this level to ensure it's remembered on wizard step change const [advancedExpanded, setAdvancedExpanded] = useState(false); const [additionalExpanded, setAdditionalExpanded] = useState(false); function getSummaryStepTitle() { - if (kibanaContext.currentSavedSearch !== null) { + if (mlContext.currentSavedSearch !== null) { return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', { defaultMessage: 'New job from saved search {title}', - values: { title: kibanaContext.currentSavedSearch.attributes.title as string }, + values: { title: mlContext.currentSavedSearch.attributes.title as string }, }); - } else if (kibanaContext.currentIndexPattern.id !== undefined) { + } else if (mlContext.currentIndexPattern.id !== undefined) { return i18n.translate( 'xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleIndexPattern', { defaultMessage: 'New job from index pattern {title}', - values: { title: kibanaContext.currentIndexPattern.title }, + values: { title: mlContext.currentIndexPattern.title }, } ); } diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 22b7d25abeae4..377ec84623480 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { ModuleJobUI, SAVE_STATE } from '../page'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { composeValidators, maxLengthValidator, @@ -52,7 +52,7 @@ export const JobSettingsForm: FC = ({ jobs, }) => { const { from, to } = getTimeFilterRange(); - const { currentIndexPattern: indexPattern } = useKibanaContext(); + const { currentIndexPattern: indexPattern } = useMlContext(); const jobPrefixValidator = composeValidators( patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/), 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 index c4a96d9e373c8..8571ae43da587 100644 --- 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 @@ -20,10 +20,10 @@ import { EuiCallOut, EuiPanel, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { merge } from 'lodash'; +import { useMlKibana } from '../../../contexts/kibana'; import { ml } from '../../../services/ml_api_service'; -import { useKibanaContext } from '../../../contexts/kibana'; +import { useMlContext } from '../../../contexts/ml'; import { DatafeedResponse, DataRecognizerConfigResponse, @@ -70,6 +70,9 @@ export enum SAVE_STATE { } export const Page: FC = ({ moduleId, existingGroupIds }) => { + const { + services: { notifications }, + } = useMlKibana(); // #region State const [jobPrefix, setJobPrefix] = useState(''); const [jobs, setJobs] = useState([]); @@ -84,7 +87,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { currentSavedSearch: savedSearch, currentIndexPattern: indexPattern, combinedQuery, - } = useKibanaContext(); + } = useMlContext(); const pageTitle = savedSearch !== null ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { @@ -206,7 +209,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { setSaveState(SAVE_STATE.FAILED); // eslint-disable-next-line no-console console.error('Error setting up module', e); - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { defaultMessage: 'Error setting up module {moduleId}', values: { moduleId }, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index cb44210b970e7..fa0ed34dca622 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache'; import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; import { KibanaObjects } from './page'; @@ -36,6 +35,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): .catch((err: Error) => { // eslint-disable-next-line no-console console.error(`Error checking whether jobs in module ${moduleId} exists`, err); + const toastNotifications = getToastNotifications(); toastNotifications.addWarning({ title: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningTitle', { defaultMessage: 'Error checking module {moduleId}', @@ -57,7 +57,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): * Gets kibana objects with an existence check. */ export const checkForSavedObjects = async (objects: KibanaObjects): Promise => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); try { return await Object.keys(objects).reduce(async (prevPromise, type) => { const acc = await prevPromise; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 0f19451b23263..835232a030383 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IUiSettingsClient } from 'src/core/public'; import { esQuery, Query, esKuery } from '../../../../../../../../../src/plugins/data/public'; import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns'; -import { KibanaConfigTypeFix } from '../../../contexts/kibana'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; import { SavedSearchSavedObject } from '../../../../../common/types/kibana'; import { getQueryFromSavedSearch } from '../../../util/index_utils'; @@ -14,7 +14,7 @@ import { getQueryFromSavedSearch } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. export function createSearchItems( - kibanaConfig: KibanaConfigTypeFix, + kibanaConfig: IUiSettingsClient, indexPattern: IIndexPattern, savedSearch: SavedSearchSavedObject | null ) { diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index c184a4d4e94e0..96e6aab377962 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -5,13 +5,13 @@ */ import React from 'react'; -// @ts-ignore No declaration file for module -import { banners } from 'ui/notify'; import { EuiCallOut } from '@elastic/eui'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; // @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 { getOverlays } from '../util/dependency_cache'; let licenseHasExpired = true; let licenseType: LICENSE_TYPE | null = null; @@ -75,9 +75,10 @@ function setLicenseExpired(features: any) { const message = features.message; if (expiredLicenseBannerId === undefined) { // Only show the banner once with no way to dismiss it - expiredLicenseBannerId = banners.add({ - component: , - }); + const overlays = getOverlays(); + expiredLicenseBannerId = overlays.banners.add( + toMountPoint() + ); } } } diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index 092639cd5fbab..a05de8b0d0880 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -12,16 +12,35 @@ import { management } from 'ui/management'; import { i18n } from '@kbn/i18n'; +import chrome from 'ui/chrome'; +import { metadata } from 'ui/metadata'; // @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 { setDependencyCache } from '../util/dependency_cache'; import './jobs_list'; if ( xpackInfo.get('features.ml.showLinks', false) === true && xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL ) { + const legacyBasePath = { + prepend: chrome.addBasePath, + get: chrome.getBasePath, + remove: () => {}, + }; + const legacyDocLinks = { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: metadata.branch, + }; + + setDependencyCache({ + docLinks: legacyDocLinks as any, + basePath: legacyBasePath as any, + XSRF: chrome.getXsrfToken(), + }); + management.register('ml', { display: i18n.translate('xpack.ml.management.mlTitle', { defaultMessage: 'Machine Learning', diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 1591dbcbad6bf..a987ed7feeee9 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -18,7 +18,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; @@ -66,12 +66,12 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { } export const JobsListPage: FC = ({ isMlEnabledInSpace }) => { + const docLinks = getDocLinks(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const tabs = getTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); - - // metadata.branch corresponds to the version used in documentation links. - const anomalyDetectionJobsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-jobs.html`; - const anomalyJobsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics.html`; + const anomalyDetectionJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`; + const anomalyJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`; const anomalyDetectionDocsLabel = i18n.translate( 'xpack.ml.management.jobsList.anomalyDetectionDocsLabel', diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 1f9d0413d45f9..cda03b21b0d65 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { toastNotifications } from 'ui/notify'; +import { useMlKibana } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; @@ -55,6 +55,9 @@ interface Props { } export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { + const { + services: { notifications }, + } = useMlKibana(); const [isLoading, setIsLoading] = useState(false); const [groups, setGroups] = useState({}); const [groupsCount, setGroupsCount] = useState(0); @@ -114,7 +117,8 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { setGroups(tempGroups); } catch (e) { - toastNotifications.addDanger( + const { toasts } = notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.overview.anomalyDetection.errorWithFetchingAnomalyScoreNotificationErrorMessage', { diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx index 8648bd211715e..219c195bab111 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx @@ -7,14 +7,10 @@ import React, { FC } from 'react'; import { EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../contexts/kibana'; const createJobLink = '#/jobs/new_job/step/index_or_search'; -// metadata.branch corresponds to the version used in documentation links. -const docsLink = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/xpack-ml.html`; const feedbackLink = 'https://www.elastic.co/community/'; -const transformsLink = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/transform`; const whatIsMachineLearningLink = 'https://www.elastic.co/what-is/elasticsearch-machine-learning'; interface Props { @@ -37,70 +33,83 @@ function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { ); } -export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => ( - - -

- -

-
- - -

- - - - ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), - transforms: ( - - - - ), - whatIsMachineLearning: ( - - - - ), - }} - /> -

-

- -

-

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

-
-
-); +export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { + const { + services: { + docLinks, + http: { basePath }, + }, + } = useMlKibana(); + + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsLink = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-ml.html`; + const transformsLink = `${basePath.get()}/app/kibana#/management/elasticsearch/transform`; + + return ( + + +

+ +

+
+ + +

+ + + + ), + createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), + transforms: ( + + + + ), + whatIsMachineLearning: ( + + + + ), + }} + /> +

+

+ +

+

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

+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts index 30c5fbc497afe..5fc1ea533e87f 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts @@ -9,7 +9,8 @@ import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; -import { PageDependencies } from './router'; + +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; export interface Resolvers { [name: string]: () => Promise; @@ -17,11 +18,16 @@ export interface Resolvers { export interface ResolverResults { [name: string]: any; } -export const basicResolvers = (deps: PageDependencies): Resolvers => ({ + +interface BasicResolverDependencies { + indexPatterns: IndexPatternsContract; +} + +export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({ checkFullLicense, getMlNodeCount, loadMlServerInfo, - loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + loadIndexPatterns: () => loadIndexPatterns(indexPatterns), checkGetJobsPrivilege, loadSavedSearches, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx index 174c1ef1d4fe8..6b56bc154e801 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx @@ -7,11 +7,11 @@ import React, { FC } from 'react'; import { HashRouter, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; -import { KibanaContext, KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { IUiSettingsClient, ChromeStart } from 'src/core/public'; +import { ChromeBreadcrumb } from 'kibana/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; +import { MlContext, MlContextValue } from '../contexts/ml'; import * as routes from './routes'; @@ -22,33 +22,30 @@ interface MlRouteProps extends RouteProps { export interface MlRoute { path: string; - render(props: MlRouteProps, config: KibanaConfigTypeFix, deps: PageDependencies): JSX.Element; + render(props: MlRouteProps, deps: PageDependencies): JSX.Element; breadcrumbs: ChromeBreadcrumb[]; } export interface PageProps { location: Location; - config: KibanaConfigTypeFix; deps: PageDependencies; } -export interface PageDependencies { +interface PageDependencies { + setBreadcrumbs: ChromeStart['setBreadcrumbs']; indexPatterns: IndexPatternsContract; + config: IUiSettingsClient; } -export const PageLoader: FC<{ context: KibanaContextValue }> = ({ context, children }) => { +export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { return context === null ? null : ( - - {children} - + {children} ); }; -export const MlRouter: FC<{ - config: KibanaConfigTypeFix; - setBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => void; - indexPatterns: IndexPatternsContract; -}> = ({ config, setBreadcrumbs, indexPatterns }) => { +export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { + const setBreadcrumbs = pageDeps.setBreadcrumbs; + return (
@@ -61,7 +58,7 @@ export const MlRouter: FC<{ window.setTimeout(() => { setBreadcrumbs(route.breadcrumbs); }); - return route.render(props, config, { indexPatterns }); + return route.render(props, pageDeps); }} /> ))} diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx index 3a2f445ac6b82..bd7fc434b36ac 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -21,12 +21,12 @@ const breadcrumbs = [ export const accessDeniedRoute: MlRoute = { path: '/access-denied', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, {}); +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, {}); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 41c286c54836c..3ca23998d5b75 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const analyticsJobExplorationRoute: MlRoute = { path: '/data_frame_analytics/exploration', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); const { _g } = queryString.parse(location.search); let globalState: any = null; try { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 31bd10f2138ad..f6d7d91884646 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -25,12 +25,12 @@ const breadcrumbs = [ export const analyticsJobsListRoute: MlRoute = { path: '/data_frame_analytics', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index 3faca285319d5..e89834018f5e6 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -23,12 +23,12 @@ const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; export const selectorRoute: MlRoute = { path: '/datavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, checkFindFileStructurePrivilege, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 11e6b85f939d3..b4ccccd0776eb 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -36,12 +36,12 @@ const breadcrumbs = [ export const fileBasedRoute: MlRoute = { path: '/filedatavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, { +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkFindFileStructurePrivilege, @@ -49,7 +49,7 @@ const PageWrapper: FC = ({ location, config, deps }) => { }); return ( - + ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index ab359238695d4..fa4745f19e3b4 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -32,13 +32,13 @@ const breadcrumbs = [ export const indexBasedRoute: MlRoute = { path: '/jobs/new_job/datavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context } = useResolver(index, savedSearchId, config, { + const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkGetJobsPrivilege, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx index adef7055f9748..b0046f7b8d699 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,8 +9,6 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -31,6 +29,7 @@ import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { useMlKibana } from '../../contexts/kibana'; const breadcrumbs = [ ML_BREADCRUMB, @@ -45,12 +44,12 @@ const breadcrumbs = [ export const explorerRoute: MlRoute = { path: '/explorer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config, deps }) => { - const { context, results } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context, results } = useResolver(undefined, undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), @@ -71,6 +70,8 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [appState, setAppState] = useUrlState('_a'); const [globalState, setGlobalState] = useUrlState('_g'); const [lastRefresh, setLastRefresh] = useState(0); + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; const { jobIds } = useJobSelection(jobsWithTimeRange, getDateFormatTz()); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx index 3d9a2adedc40d..2f4df2d5a307a 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const jobListRoute: MlRoute = { path: '/jobs', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config, deps }) => { - const { context } = useResolver(undefined, undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); const [globalState, setGlobalState] = useUrlState('_g'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b81058a9c89af..ae35d783517d3 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -6,12 +6,11 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { MlRoute, PageLoader, PageDependencies } from '../../router'; +import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -import { KibanaConfigTypeFix } from '../../../contexts/kibana'; import { checkBasicLicense } from '../../../license/check_license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; @@ -22,6 +21,11 @@ enum MODE { DATAVISUALIZER, } +interface IndexOrSearchPageProps extends PageProps { + nextStepPath: string; + mode: MODE; +} + const breadcrumbs = [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, @@ -35,9 +39,9 @@ const breadcrumbs = [ export const indexOrSearchRoute: MlRoute = { path: '/jobs/new_job/step/index_or_search', - render: (props, config, deps) => ( + render: (props, deps) => ( ( + render: (props, deps) => ( = ({ config, nextStepPath, deps, mode }) => { +const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { ...basicResolvers(deps), preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), @@ -79,7 +78,7 @@ const PageWrapper: FC<{ const { context } = useResolver( undefined, undefined, - config, + deps.config, mode === MODE.NEW_JOB ? newJobResolvers : dataVizResolvers ); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index e537a186ec784..c2e87f065116e 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -28,13 +28,13 @@ const breadcrumbs = [ export const jobTypeRoute: MlRoute = { path: '/jobs/new_job/step/job_type', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context } = useResolver(index, savedSearchId, config, basicResolvers(deps)); + const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 4f5085facfb29..78f72a7b7a39b 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -30,21 +30,19 @@ const breadcrumbs = [ export const recognizeRoute: MlRoute = { path: '/jobs/new_job/recognize', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; export const checkViewOrCreateRoute: MlRoute = { path: '/modules/check_view_or_create', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: [], }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId } = queryString.parse(location.search); - const { context, results } = useResolver(index, savedSearchId, config, { + const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); @@ -56,10 +54,10 @@ const PageWrapper: FC = ({ location, config, deps }) => { ); }; -const CheckViewOrCreateWrapper: FC = ({ location, config, deps }) => { +const CheckViewOrCreateWrapper: FC = ({ location, deps }) => { const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); // the single resolver checkViewOrCreateJobs redirects only. so will always reject - useResolver(undefined, undefined, config, { + useResolver(undefined, undefined, deps.config, { checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), }); return null; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 99c0511cd09ce..230d96456427c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -84,47 +84,37 @@ const categorizationBreadcrumbs = [ export const singleMetricRoute: MlRoute = { path: '/jobs/new_job/single_metric', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: singleMetricBreadcrumbs, }; export const multiMetricRoute: MlRoute = { path: '/jobs/new_job/multi_metric', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: multiMetricBreadcrumbs, }; export const populationRoute: MlRoute = { path: '/jobs/new_job/population', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: populationBreadcrumbs, }; export const advancedRoute: MlRoute = { path: '/jobs/new_job/advanced', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: advancedBreadcrumbs, }; export const categorizationRoute: MlRoute = { path: '/jobs/new_job/categorization', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: categorizationBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, jobType, deps }) => { +const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context, results } = useResolver(index, savedSearchId, config, { + const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), privileges: checkCreateJobsPrivilege, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx index fe9f4336148f3..85227c11582d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const overviewRoute: MlRoute = { path: '/overview', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 56ff57f6610b2..fdbfcb3397c75 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -34,12 +34,12 @@ const breadcrumbs = [ export const calendarListRoute: MlRoute = { path: '/settings/calendars_list', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index fb68f103e1b77..7f622a1bba62b 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -54,28 +54,24 @@ const editBreadcrumbs = [ export const newCalendarRoute: MlRoute = { path: '/settings/calendars_list/new_calendar', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: newBreadcrumbs, }; export const editCalendarRoute: MlRoute = { path: '/settings/calendars_list/edit_calendar/:calendarId', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: editBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, mode }) => { +const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; if (mode === MODE.EDIT) { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } - const { context } = useResolver(undefined, undefined, config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, checkMlNodesAvailable, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index cb19883e962c1..6a4ce271bff17 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -35,12 +35,12 @@ const breadcrumbs = [ export const filterListRoute: MlRoute = { path: '/settings/filter_lists', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7a596a488ddb6..4fa15ebaac21a 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -54,28 +54,24 @@ const editBreadcrumbs = [ export const newFilterListRoute: MlRoute = { path: '/settings/filter_lists/new_filter_list', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: newBreadcrumbs, }; export const editFilterListRoute: MlRoute = { path: '/settings/filter_lists/edit_filter_list/:filterId', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: editBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, mode }) => { +const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; if (mode === MODE.EDIT) { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } - const { context } = useResolver(undefined, undefined, config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, checkMlNodesAvailable, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx index b62ecc0539e72..846512503ede5 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -24,12 +24,12 @@ const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; export const settingsRoute: MlRoute = { path: '/settings', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 6917ec718d3a8..0ae42aa44e089 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -12,7 +12,69 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; -jest.mock('ui/new_platform'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + query: { + timefilter: { + timefilter: { + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + }, + }, +})); + +jest.mock('../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + uiSettings: { get: jest.fn() }, + data: { + query: { + timefilter: { + timefilter: { + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + notifications: { + toasts: { + addDanger: () => {}, + }, + }, + }, + }; + }, +})); + +jest.mock('../../util/dependency_cache', () => ({ + getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), +})); describe('TimeSeriesExplorerUrlStateManager', () => { test('Initial render shows "No single metric jobs found"', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 4455e6e99ada7..2bf3d50c3678c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -13,8 +13,6 @@ import queryString from 'query-string'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -39,10 +37,11 @@ import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { useMlKibana } from '../../contexts/kibana'; export const timeSeriesExplorerRoute: MlRoute = { path: '/timeseriesexplorer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs: [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, @@ -55,8 +54,8 @@ export const timeSeriesExplorerRoute: MlRoute = { ], }; -const PageWrapper: FC = ({ config, deps }) => { - const { context, results } = useResolver('', undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context, results } = useResolver('', undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), @@ -65,7 +64,7 @@ const PageWrapper: FC = ({ config, deps }) => { return ( @@ -91,6 +90,8 @@ export const TimeSeriesExplorerUrlStateManager: FC(); + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; const refresh = useRefresh(); useEffect(() => { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts index 3716b9715bb5b..ee4f77767fce8 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts @@ -5,6 +5,7 @@ */ import { useEffect, useState } from 'react'; +import { IUiSettingsClient } from 'src/core/public'; import { getIndexPatternById, getIndexPatternsContract, @@ -12,14 +13,14 @@ import { } from '../util/index_utils'; import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; -import { KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; +import { MlContextValue } from '../contexts/ml'; export const useResolver = ( indexPatternId: string | undefined, savedSearchId: string | undefined, - config: KibanaConfigTypeFix, + config: IUiSettingsClient, resolvers: Resolvers -): { context: KibanaContextValue; results: ResolverResults } => { +): { context: MlContextValue; results: ResolverResults } => { const funcNames = Object.keys(resolvers); // Object.entries gets this wrong?! const funcs = Object.values(resolvers); // Object.entries gets this wrong?! const tempResults = funcNames.reduce((p, c) => { 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 index 41200759b7c8a..73a30dbcd71b2 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -6,24 +6,23 @@ // 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'; +import { getXSRF } from '../util/dependency_cache'; + export interface HttpOptions { url?: string; } function getResultHeaders(headers: HeadersInit): HeadersInit { - return addSystemApiHeader({ + return { + asSystemRequest: false, 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), + 'kbn-version': getXSRF(), ...headers, - }); + } as HeadersInit; } export function http(options: any) { @@ -31,11 +30,7 @@ export function http(options: any) { 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 headers = getResultHeaders(options.headers ?? {}); const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers }; 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 index 54d55159646f6..cc30d481a6355 100644 --- 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 @@ -4,12 +4,9 @@ * 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'); +import { basePath } from './index'; export const annotations = { getAnnotations(obj: { @@ -18,21 +15,21 @@ export const annotations = { latestMs: number; maxAnnotations: number; }) { - return http$<{ annotations: Record }>(`${basePath}/annotations`, { + return http$<{ annotations: Record }>(`${basePath()}/annotations`, { method: 'POST', body: obj, }); }, indexAnnotation(obj: any) { return http({ - url: `${basePath}/annotations/index`, + url: `${basePath()}/annotations/index`, method: 'PUT', data: obj, }); }, deleteAnnotation(id: string) { return http({ - url: `${basePath}/annotations/delete/${id}`, + url: `${basePath()}/annotations/delete/${id}`, method: 'DELETE', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js index 6ff0b45454abf..8a74cddce3f6d 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js @@ -4,75 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const dataFrameAnalytics = { getDataFrameAnalytics(analyticsId) { const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; return http({ - url: `${basePath}/data_frame/analytics${analyticsIdString}`, + url: `${basePath()}/data_frame/analytics${analyticsIdString}`, method: 'GET', }); }, getDataFrameAnalyticsStats(analyticsId) { if (analyticsId !== undefined) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_stats`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_stats`, method: 'GET', }); } return http({ - url: `${basePath}/data_frame/analytics/_stats`, + url: `${basePath()}/data_frame/analytics/_stats`, method: 'GET', }); }, createDataFrameAnalytics(analyticsId, analyticsConfig) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}`, method: 'PUT', data: analyticsConfig, }); }, evaluateDataFrameAnalytics(evaluateConfig) { return http({ - url: `${basePath}/data_frame/_evaluate`, + url: `${basePath()}/data_frame/_evaluate`, method: 'POST', data: evaluateConfig, }); }, explainDataFrameAnalytics(jobConfig) { return http({ - url: `${basePath}/data_frame/analytics/_explain`, + url: `${basePath()}/data_frame/analytics/_explain`, method: 'POST', data: jobConfig, }); }, deleteDataFrameAnalytics(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}`, method: 'DELETE', }); }, startDataFrameAnalytics(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_start`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, method: 'POST', }); }, stopDataFrameAnalytics(analyticsId, force = false) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_stop?force=${force}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_stop?force=${force}`, method: 'POST', }); }, getAnalyticsAuditMessages(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/messages`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/messages`, method: 'GET', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js index c9f6bc08e75ec..364fa57ba7d6b 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const fileDatavisualizer = { analyzeFile(obj, params = {}) { @@ -22,7 +20,7 @@ export const fileDatavisualizer = { } } return http({ - url: `${basePath}/file_data_visualizer/analyze_file${paramString}`, + url: `${basePath()}/file_data_visualizer/analyze_file${paramString}`, method: 'POST', data: obj, }); @@ -33,7 +31,7 @@ export const fileDatavisualizer = { const { index, data, settings, mappings, ingestPipeline } = obj; return http({ - url: `${basePath}/file_data_visualizer/import${paramString}`, + url: `${basePath()}/file_data_visualizer/import${paramString}`, method: 'POST', data: { index, 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 index 1377ca7e60261..010a531a192f1 100644 --- 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 @@ -7,31 +7,29 @@ // 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'); +import { basePath } from './index'; export const filters = { filters(obj) { const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; return http({ - url: `${basePath}/filters${filterId}`, + url: `${basePath()}/filters${filterId}`, method: 'GET', }); }, filtersStats() { return http({ - url: `${basePath}/filters/_stats`, + url: `${basePath()}/filters/_stats`, method: 'GET', }); }, addFilter(filterId, description, items) { return http({ - url: `${basePath}/filters`, + url: `${basePath()}/filters`, method: 'PUT', data: { filterId, @@ -54,7 +52,7 @@ export const filters = { } return http({ - url: `${basePath}/filters/${filterId}`, + url: `${basePath()}/filters/${filterId}`, method: 'PUT', data, }); @@ -62,7 +60,7 @@ export const filters = { deleteFilter(filterId) { return http({ - url: `${basePath}/filters/${filterId}`, + 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 index 6420b60e4c838..6cb8eccafe151 100644 --- 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 @@ -29,6 +29,8 @@ import { } from '../../../../common/types/categories'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; +declare const basePath: () => string; + // 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. 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 index 565cf0c0bfa8b..6fdc76d7244d3 100644 --- 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 @@ -5,8 +5,6 @@ */ import { pick } from 'lodash'; -import chrome from 'ui/chrome'; - import { http, http$ } from '../http_service'; import { annotations } from './annotations'; @@ -15,27 +13,30 @@ import { filters } from './filters'; import { results } from './results'; import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; +import { getBasePath } from '../../util/dependency_cache'; -const basePath = chrome.addBasePath('/api/ml'); +export function basePath() { + return getBasePath().prepend('/api/ml'); +} export const ml = { getJobs(obj) { const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; return http({ - url: `${basePath}/anomaly_detectors${jobId}`, + url: `${basePath()}/anomaly_detectors${jobId}`, }); }, getJobStats(obj) { const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; return http({ - url: `${basePath}/anomaly_detectors${jobId}/_stats`, + url: `${basePath()}/anomaly_detectors${jobId}/_stats`, }); }, addJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}`, method: 'PUT', data: obj.job, }); @@ -43,35 +44,35 @@ export const ml = { openJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_open`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_open`, method: 'POST', }); }, closeJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_close`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_close`, method: 'POST', }); }, deleteJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}`, method: 'DELETE', }); }, forceDeleteJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}?force=true`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}?force=true`, method: 'DELETE', }); }, updateJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_update`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_update`, method: 'POST', data: obj.job, }); @@ -79,7 +80,7 @@ export const ml = { estimateBucketSpan(obj) { return http({ - url: `${basePath}/validate/estimate_bucket_span`, + url: `${basePath()}/validate/estimate_bucket_span`, method: 'POST', data: obj, }); @@ -87,14 +88,14 @@ export const ml = { validateJob(obj) { return http({ - url: `${basePath}/validate/job`, + url: `${basePath()}/validate/job`, method: 'POST', data: obj, }); }, validateCardinality$(obj) { - return http$(`${basePath}/validate/cardinality`, { + return http$(`${basePath()}/validate/cardinality`, { method: 'POST', body: obj, }); @@ -103,20 +104,20 @@ export const ml = { getDatafeeds(obj) { const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; return http({ - url: `${basePath}/datafeeds${datafeedId}`, + url: `${basePath()}/datafeeds${datafeedId}`, }); }, getDatafeedStats(obj) { const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; return http({ - url: `${basePath}/datafeeds${datafeedId}/_stats`, + url: `${basePath()}/datafeeds${datafeedId}/_stats`, }); }, addDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, + url: `${basePath()}/datafeeds/${obj.datafeedId}`, method: 'PUT', data: obj.datafeedConfig, }); @@ -124,7 +125,7 @@ export const ml = { updateDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_update`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_update`, method: 'POST', data: obj.datafeedConfig, }); @@ -132,14 +133,14 @@ export const ml = { deleteDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, + url: `${basePath()}/datafeeds/${obj.datafeedId}`, method: 'DELETE', }); }, forceDeleteDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}?force=true`, + url: `${basePath()}/datafeeds/${obj.datafeedId}?force=true`, method: 'DELETE', }); }, @@ -153,7 +154,7 @@ export const ml = { data.end = obj.end; } return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_start`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_start`, method: 'POST', data, }); @@ -161,21 +162,21 @@ export const ml = { stopDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_stop`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_stop`, method: 'POST', }); }, datafeedPreview(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_preview`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_preview`, method: 'GET', }); }, validateDetector(obj) { return http({ - url: `${basePath}/anomaly_detectors/_validate/detector`, + url: `${basePath()}/anomaly_detectors/_validate/detector`, method: 'POST', data: obj.detector, }); @@ -188,7 +189,7 @@ export const ml = { } return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_forecast`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_forecast`, method: 'POST', data, }); @@ -197,7 +198,7 @@ export const ml = { overallBuckets(obj) { const data = pick(obj, ['topN', 'bucketSpan', 'start', 'end']); return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, method: 'POST', data, }); @@ -205,7 +206,7 @@ export const ml = { hasPrivileges(obj) { return http({ - url: `${basePath}/_has_privileges`, + url: `${basePath()}/_has_privileges`, method: 'POST', data: obj, }); @@ -213,21 +214,21 @@ export const ml = { checkMlPrivileges() { return http({ - url: `${basePath}/ml_capabilities`, + url: `${basePath()}/ml_capabilities`, method: 'GET', }); }, checkManageMLPrivileges() { return http({ - url: `${basePath}/ml_capabilities?ignoreSpaces=true`, + url: `${basePath()}/ml_capabilities?ignoreSpaces=true`, method: 'GET', }); }, getNotificationSettings() { return http({ - url: `${basePath}/notification_settings`, + url: `${basePath()}/notification_settings`, method: 'GET', }); }, @@ -241,7 +242,7 @@ export const ml = { data.fields = obj.fields; } return http({ - url: `${basePath}/indices/field_caps`, + url: `${basePath()}/indices/field_caps`, method: 'POST', data, }); @@ -249,28 +250,28 @@ export const ml = { recognizeIndex(obj) { return http({ - url: `${basePath}/modules/recognize/${obj.indexPatternTitle}`, + url: `${basePath()}/modules/recognize/${obj.indexPatternTitle}`, method: 'GET', }); }, listDataRecognizerModules() { return http({ - url: `${basePath}/modules/get_module`, + url: `${basePath()}/modules/get_module`, method: 'GET', }); }, getDataRecognizerModule(obj) { return http({ - url: `${basePath}/modules/get_module/${obj.moduleId}`, + url: `${basePath()}/modules/get_module/${obj.moduleId}`, method: 'GET', }); }, dataRecognizerModuleJobsExist(obj) { return http({ - url: `${basePath}/modules/jobs_exist/${obj.moduleId}`, + url: `${basePath()}/modules/jobs_exist/${obj.moduleId}`, method: 'GET', }); }, @@ -289,7 +290,7 @@ export const ml = { ]); return http({ - url: `${basePath}/modules/setup/${obj.moduleId}`, + url: `${basePath()}/modules/setup/${obj.moduleId}`, method: 'POST', data, }); @@ -308,7 +309,7 @@ export const ml = { ]); return http({ - url: `${basePath}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, + url: `${basePath()}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, method: 'POST', data, }); @@ -326,7 +327,7 @@ export const ml = { ]); return http({ - url: `${basePath}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, + url: `${basePath()}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, method: 'POST', data, }); @@ -346,14 +347,14 @@ export const ml = { calendarIdsPathComponent = `/${calendarIds.join(',')}`; } return http({ - url: `${basePath}/calendars${calendarIdsPathComponent}`, + url: `${basePath()}/calendars${calendarIdsPathComponent}`, method: 'GET', }); }, addCalendar(obj) { return http({ - url: `${basePath}/calendars`, + url: `${basePath()}/calendars`, method: 'PUT', data: obj, }); @@ -362,7 +363,7 @@ export const ml = { updateCalendar(obj) { const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : ''; return http({ - url: `${basePath}/calendars${calendarId}`, + url: `${basePath()}/calendars${calendarId}`, method: 'PUT', data: obj, }); @@ -370,21 +371,21 @@ export const ml = { deleteCalendar(obj) { return http({ - url: `${basePath}/calendars/${obj.calendarId}`, + url: `${basePath()}/calendars/${obj.calendarId}`, method: 'DELETE', }); }, mlNodeCount() { return http({ - url: `${basePath}/ml_node_count`, + url: `${basePath()}/ml_node_count`, method: 'GET', }); }, mlInfo() { return http({ - url: `${basePath}/info`, + url: `${basePath()}/info`, method: 'GET', }); }, @@ -402,7 +403,7 @@ export const ml = { ]); return http({ - url: `${basePath}/validate/calculate_model_memory_limit`, + url: `${basePath()}/validate/calculate_model_memory_limit`, method: 'POST', data, }); @@ -419,7 +420,7 @@ export const ml = { ]); return http({ - url: `${basePath}/fields_service/field_cardinality`, + url: `${basePath()}/fields_service/field_cardinality`, method: 'POST', data, }); @@ -429,7 +430,7 @@ export const ml = { const data = pick(obj, ['index', 'timeFieldName', 'query']); return http({ - url: `${basePath}/fields_service/time_field_range`, + url: `${basePath()}/fields_service/time_field_range`, method: 'POST', data, }); @@ -437,21 +438,21 @@ export const ml = { esSearch(obj) { return http({ - url: `${basePath}/es_search`, + url: `${basePath()}/es_search`, method: 'POST', data: obj, }); }, esSearch$(obj) { - return http$(`${basePath}/es_search`, { + return http$(`${basePath()}/es_search`, { method: 'POST', body: obj, }); }, getIndices() { - const tempBasePath = chrome.addBasePath('/api'); + const tempBasePath = getBasePath().prepend('/api'); return http({ url: `${tempBasePath}/index_management/indices`, method: 'GET', diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index 05d98dc1a1e64..cc9593d946bd1 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const jobs = { jobsSummary(jobIds) { return http({ - url: `${basePath}/jobs/jobs_summary`, + url: `${basePath()}/jobs/jobs_summary`, method: 'POST', data: { jobIds, @@ -23,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_timerange`, method: 'POST', data: { dateFormatTz, @@ -33,7 +31,7 @@ export const jobs = { jobs(jobIds) { return http({ - url: `${basePath}/jobs/jobs`, + url: `${basePath()}/jobs/jobs`, method: 'POST', data: { jobIds, @@ -43,14 +41,14 @@ export const jobs = { groups() { return http({ - url: `${basePath}/jobs/groups`, + url: `${basePath()}/jobs/groups`, method: 'GET', }); }, updateGroups(updatedJobs) { return http({ - url: `${basePath}/jobs/update_groups`, + url: `${basePath()}/jobs/update_groups`, method: 'POST', data: { jobs: updatedJobs, @@ -60,7 +58,7 @@ export const jobs = { forceStartDatafeeds(datafeedIds, start, end) { return http({ - url: `${basePath}/jobs/force_start_datafeeds`, + url: `${basePath()}/jobs/force_start_datafeeds`, method: 'POST', data: { datafeedIds, @@ -72,7 +70,7 @@ export const jobs = { stopDatafeeds(datafeedIds) { return http({ - url: `${basePath}/jobs/stop_datafeeds`, + url: `${basePath()}/jobs/stop_datafeeds`, method: 'POST', data: { datafeedIds, @@ -82,7 +80,7 @@ export const jobs = { deleteJobs(jobIds) { return http({ - url: `${basePath}/jobs/delete_jobs`, + url: `${basePath()}/jobs/delete_jobs`, method: 'POST', data: { jobIds, @@ -92,7 +90,7 @@ export const jobs = { closeJobs(jobIds) { return http({ - url: `${basePath}/jobs/close_jobs`, + url: `${basePath()}/jobs/close_jobs`, method: 'POST', data: { jobIds, @@ -104,21 +102,21 @@ export const jobs = { const jobIdString = jobId !== undefined ? `/${jobId}` : ''; const fromString = from !== undefined ? `?from=${from}` : ''; return http({ - url: `${basePath}/job_audit_messages/messages${jobIdString}${fromString}`, + url: `${basePath()}/job_audit_messages/messages${jobIdString}${fromString}`, method: 'GET', }); }, deletingJobTasks() { return http({ - url: `${basePath}/jobs/deleting_jobs_tasks`, + url: `${basePath()}/jobs/deleting_jobs_tasks`, method: 'GET', }); }, jobsExist(jobIds) { return http({ - url: `${basePath}/jobs/jobs_exist`, + url: `${basePath()}/jobs/jobs_exist`, method: 'POST', data: { jobIds, @@ -129,7 +127,7 @@ export const jobs = { newJobCaps(indexPatternTitle, isRollup = false) { const isRollupString = isRollup === true ? `?rollup=true` : ''; return http({ - url: `${basePath}/jobs/new_job_caps/${indexPatternTitle}${isRollupString}`, + url: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}${isRollupString}`, method: 'GET', }); }, @@ -146,7 +144,7 @@ export const jobs = { splitFieldValue ) { return http({ - url: `${basePath}/jobs/new_job_line_chart`, + url: `${basePath()}/jobs/new_job_line_chart`, method: 'POST', data: { indexPatternTitle, @@ -173,7 +171,7 @@ export const jobs = { splitFieldName ) { return http({ - url: `${basePath}/jobs/new_job_population_chart`, + url: `${basePath()}/jobs/new_job_population_chart`, method: 'POST', data: { indexPatternTitle, @@ -190,14 +188,14 @@ export const jobs = { getAllJobAndGroupIds() { return http({ - url: `${basePath}/jobs/all_jobs_and_group_ids`, + url: `${basePath()}/jobs/all_jobs_and_group_ids`, method: 'GET', }); }, getLookBackProgress(jobId, start, end) { return http({ - url: `${basePath}/jobs/look_back_progress`, + url: `${basePath()}/jobs/look_back_progress`, method: 'POST', data: { jobId, @@ -218,7 +216,7 @@ export const jobs = { analyzer ) { return http({ - url: `${basePath}/jobs/categorization_field_examples`, + url: `${basePath()}/jobs/categorization_field_examples`, method: 'POST', data: { indexPatternTitle, @@ -235,7 +233,7 @@ export const jobs = { topCategories(jobId, count) { return http({ - url: `${basePath}/jobs/top_categories`, + url: `${basePath()}/jobs/top_categories`, method: 'POST', data: { jobId, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 38ae777106680..e770e80f4c4d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -6,11 +6,9 @@ // Service for obtaining data for the ML Results dashboards. -import chrome from 'ui/chrome'; - import { http, http$ } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const results = { getAnomaliesTableData( @@ -26,7 +24,7 @@ export const results = { maxExamples, influencersFilterQuery ) { - return http$(`${basePath}/results/anomalies_table_data`, { + return http$(`${basePath()}/results/anomalies_table_data`, { method: 'POST', body: { jobIds, @@ -46,7 +44,7 @@ export const results = { getMaxAnomalyScore(jobIds, earliestMs, latestMs) { return http({ - url: `${basePath}/results/max_anomaly_score`, + url: `${basePath()}/results/max_anomaly_score`, method: 'POST', data: { jobIds, @@ -58,7 +56,7 @@ export const results = { getCategoryDefinition(jobId, categoryId) { return http({ - url: `${basePath}/results/category_definition`, + url: `${basePath()}/results/category_definition`, method: 'POST', data: { jobId, categoryId }, }); @@ -66,7 +64,7 @@ export const results = { getCategoryExamples(jobId, categoryIds, maxExamples) { return http({ - url: `${basePath}/results/category_examples`, + url: `${basePath()}/results/category_examples`, method: 'POST', data: { jobId, @@ -77,7 +75,7 @@ export const results = { }, fetchPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs) { - return http$(`${basePath}/results/partition_fields_values`, { + return http$(`${basePath()}/results/partition_fields_values`, { method: 'POST', body: { jobId, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index e8f7050f20875..2f5eb596a157b 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -14,7 +14,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` horizontalPosition="center" verticalPosition="center" > - - { + const msg = i18n.translate('xpack.ml.calendarsEdit.calendarForm.allowedCharactersDescription', { defaultMessage: 'Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character', @@ -217,9 +216,9 @@ export const CalendarForm = injectI18n(function CalendarForm({ ); -}); +}; -CalendarForm.WrappedComponent.propTypes = { +CalendarForm.propTypes = { calendarId: PropTypes.string.isRequired, canCreateCalendar: PropTypes.bool.isRequired, canDeleteCalendar: PropTypes.bool.isRequired, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 6befb9987cba8..bc055bffe9973 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { CalendarForm } from './calendar_form'; @@ -39,7 +35,7 @@ const testProps = { describe('CalendarForm', () => { test('Renders calendar form', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -51,7 +47,7 @@ describe('CalendarForm', () => { calendarId: 'test-calendar', description: 'test description', }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const calendarId = wrapper.find('EuiTitle'); expect(calendarId).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index 125c75d438af9..7a05a4ccb6aa7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -10,7 +10,8 @@ import moment from 'moment'; import { EuiButton, EuiButtonEmpty, EuiInMemoryTable, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -32,7 +33,7 @@ function DeleteButton({ onClick, canDeleteCalendar }) { ); } -export const EventsTable = injectI18n(function EventsTable({ +export const EventsTable = ({ canCreateCalendar, canDeleteCalendar, eventsList, @@ -40,8 +41,7 @@ export const EventsTable = injectI18n(function EventsTable({ showSearchBar, showImportModal, showNewEventModal, - intl, -}) { +}) => { const sorting = { sort: { field: 'description', @@ -57,8 +57,7 @@ export const EventsTable = injectI18n(function EventsTable({ const columns = [ { field: 'description', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.descriptionColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.descriptionColumnName', { defaultMessage: 'Description', }), sortable: true, @@ -67,8 +66,7 @@ export const EventsTable = injectI18n(function EventsTable({ }, { field: 'start_time', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.startColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.startColumnName', { defaultMessage: 'Start', }), sortable: true, @@ -79,8 +77,7 @@ export const EventsTable = injectI18n(function EventsTable({ }, { field: 'end_time', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.endColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.endColumnName', { defaultMessage: 'End', }), sortable: true, @@ -152,9 +149,9 @@ export const EventsTable = injectI18n(function EventsTable({ /> ); -}); +}; -EventsTable.WrappedComponent.propTypes = { +EventsTable.propTypes = { canCreateCalendar: PropTypes.bool, canDeleteCalendar: PropTypes.bool, eventsList: PropTypes.array.isRequired, @@ -164,7 +161,7 @@ EventsTable.WrappedComponent.propTypes = { showSearchBar: PropTypes.bool, }; -EventsTable.WrappedComponent.defaultProps = { +EventsTable.defaultProps = { showSearchBar: false, canCreateCalendar: true, canDeleteCalendar: true, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 851ce52d68a36..8336a2d286639 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { EventsTable } from './events_table'; @@ -31,7 +27,7 @@ const testProps = { describe('EventsTable', () => { test('Renders events table with no search bar', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -42,7 +38,7 @@ describe('EventsTable', () => { showSearchBar: true, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js index 5e2547ffa64e4..47644e329805c 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js @@ -23,191 +23,194 @@ import { import { ImportedEvents } from '../imported_events'; import { readFile, parseICSFile, filterEvents } from './utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const MAX_FILE_SIZE_MB = 100; -export const ImportModal = injectI18n( - class ImportModal extends Component { - static propTypes = { - addImportedEvents: PropTypes.func.isRequired, - closeImportModal: PropTypes.func.isRequired, +export class ImportModal extends Component { + static propTypes = { + addImportedEvents: PropTypes.func.isRequired, + closeImportModal: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + includePastEvents: false, + allImportedEvents: [], + selectedEvents: [], + fileLoading: false, + fileLoaded: false, + errorMessage: null, }; + } - constructor(props) { - super(props); - - this.state = { - includePastEvents: false, - allImportedEvents: [], - selectedEvents: [], - fileLoading: false, - fileLoaded: false, - errorMessage: null, - }; - } - - handleImport = async loadedFile => { - const incomingFile = loadedFile[0]; - const errorMessage = this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.importModal.couldNotParseICSFileErrorMessage', + handleImport = async loadedFile => { + const incomingFile = loadedFile[0]; + const errorMessage = i18n.translate( + 'xpack.ml.calendarsEdit.importModal.couldNotParseICSFileErrorMessage', + { defaultMessage: 'Could not parse ICS file.', - }); - let events = []; - - if (incomingFile && incomingFile.size <= MAX_FILE_SIZE_MB * 1000000) { - this.setState({ fileLoading: true, fileLoaded: true }); - - try { - const parsedFile = await readFile(incomingFile); - events = parseICSFile(parsedFile.data); - - this.setState({ - allImportedEvents: events, - selectedEvents: filterEvents(events), - fileLoading: false, - errorMessage: null, - includePastEvents: false, - }); - } catch (error) { - console.log(errorMessage, error); - this.setState({ errorMessage, fileLoading: false }); - } - } else if (incomingFile && incomingFile.size > MAX_FILE_SIZE_MB * 1000000) { - this.setState({ fileLoading: false, errorMessage }); - } else { - this.setState({ fileLoading: false, errorMessage: null }); } - }; - - onEventDelete = eventId => { - this.setState(prevState => ({ - allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId), - selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId), - })); - }; - - onCheckboxToggle = e => { - this.setState({ - includePastEvents: e.target.checked, - }); - }; - - handleEventsAdd = () => { - const { allImportedEvents, selectedEvents, includePastEvents } = this.state; - const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents; - - const events = eventsToImport.map(event => ({ - description: event.description, - start_time: event.start_time, - end_time: event.end_time, - event_id: event.event_id, - })); - - this.props.addImportedEvents(events); - }; - - renderCallout = () => ( - -

{this.state.errorMessage}

-
); - - render() { - const { closeImportModal, intl } = this.props; - const { - fileLoading, - fileLoaded, - allImportedEvents, - selectedEvents, - errorMessage, - includePastEvents, - } = this.state; - - let showRecurringWarning = false; - let importedEvents; - - if (includePastEvents) { - importedEvents = allImportedEvents; - } else { - importedEvents = selectedEvents; + let events = []; + + if (incomingFile && incomingFile.size <= MAX_FILE_SIZE_MB * 1000000) { + this.setState({ fileLoading: true, fileLoaded: true }); + + try { + const parsedFile = await readFile(incomingFile); + events = parseICSFile(parsedFile.data); + + this.setState({ + allImportedEvents: events, + selectedEvents: filterEvents(events), + fileLoading: false, + errorMessage: null, + includePastEvents: false, + }); + } catch (error) { + console.log(errorMessage, error); + this.setState({ errorMessage, fileLoading: false }); } + } else if (incomingFile && incomingFile.size > MAX_FILE_SIZE_MB * 1000000) { + this.setState({ fileLoading: false, errorMessage }); + } else { + this.setState({ fileLoading: false, errorMessage: null }); + } + }; + + onEventDelete = eventId => { + this.setState(prevState => ({ + allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId), + selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId), + })); + }; + + onCheckboxToggle = e => { + this.setState({ + includePastEvents: e.target.checked, + }); + }; + + handleEventsAdd = () => { + const { allImportedEvents, selectedEvents, includePastEvents } = this.state; + const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents; + + const events = eventsToImport.map(event => ({ + description: event.description, + start_time: event.start_time, + end_time: event.end_time, + event_id: event.event_id, + })); + + this.props.addImportedEvents(events); + }; + + renderCallout = () => ( + +

{this.state.errorMessage}

+
+ ); + + render() { + const { closeImportModal } = this.props; + const { + fileLoading, + fileLoaded, + allImportedEvents, + selectedEvents, + errorMessage, + includePastEvents, + } = this.state; + + let showRecurringWarning = false; + let importedEvents; + + if (includePastEvents) { + importedEvents = allImportedEvents; + } else { + importedEvents = selectedEvents; + } - if (importedEvents.find(e => e.asterisk) !== undefined) { - showRecurringWarning = true; - } + if (importedEvents.find(e => e.asterisk) !== undefined) { + showRecurringWarning = true; + } - return ( - - - - - - - - - - -

- -

-
-
-
- - - - - + + + + + + - - {errorMessage !== null && this.renderCallout()} - {allImportedEvents.length > 0 && ( - + + +

+ - )} - - - - - - + + + + + + + + - - - + {errorMessage !== null && this.renderCallout()} + {allImportedEvents.length > 0 && ( + - - - - - ); - } + )} + + + + + + + + + + + + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index b689895b05671..d20dc9d297eb2 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -33,13 +33,13 @@ const events = [ describe('ImportModal', () => { test('Renders import modal', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Deletes selected event from event table', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const testState = { allImportedEvents: events, diff --git a/x-pack/legacy/plugins/ml/public/application/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 index a4da960cbd627..a47405cd8de14 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -23,7 +23,9 @@ exports[`ImportedEvents Renders imported events 1`] = ` - ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ImportedEvents } from './imported_events'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index bc60e9e5df24e..0489528fa0f63 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -6,14 +6,11 @@ import React, { Component, Fragment } from 'react'; import { PropTypes } from 'prop-types'; -import { timefilter } from 'ui/timefilter'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiPage, EuiPageBody, EuiPageContent, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { NavigationMenu } from '../../../components/navigation_menu'; import { getCalendarSettingsData, validateCalendarId } from './utils'; @@ -21,357 +18,350 @@ import { CalendarForm } from './calendar_form'; import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; - -export const NewCalendar = injectI18n( - class NewCalendar extends Component { - static propTypes = { - calendarId: PropTypes.string, - canCreateCalendar: PropTypes.bool.isRequired, - canDeleteCalendar: PropTypes.bool.isRequired, +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; + +class NewCalendarUI extends Component { + static propTypes = { + calendarId: PropTypes.string, + canCreateCalendar: PropTypes.bool.isRequired, + canDeleteCalendar: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isNewEventModalVisible: false, + isImportModalVisible: false, + isNewCalendarIdValid: null, + loading: true, + jobIds: [], + jobIdOptions: [], + groupIds: [], + groupIdOptions: [], + calendars: [], + formCalendarId: '', + description: '', + selectedJobOptions: [], + selectedGroupOptions: [], + events: [], + saving: false, + selectedCalendar: undefined, }; + } - constructor(props) { - super(props); - this.state = { - isNewEventModalVisible: false, - isImportModalVisible: false, - isNewCalendarIdValid: null, - loading: true, - jobIds: [], - jobIdOptions: [], - groupIds: [], - groupIdOptions: [], - calendars: [], - formCalendarId: '', - description: '', - selectedJobOptions: [], - selectedGroupOptions: [], - events: [], - saving: false, - selectedCalendar: undefined, - }; - } - - componentDidMount() { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - this.formSetup(); - } + componentDidMount() { + const { timefilter } = this.props.kibana.services.data.query.timefilter; + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + this.formSetup(); + } - async formSetup() { - try { - const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); - - const jobIdOptions = jobIds.map(jobId => ({ label: jobId })); - const groupIdOptions = groupIds.map(groupId => ({ label: groupId })); - - const selectedJobOptions = []; - const selectedGroupOptions = []; - let eventsList = []; - let selectedCalendar; - let formCalendarId = ''; - - // Editing existing calendar. - if (this.props.calendarId !== undefined) { - selectedCalendar = calendars.find(cal => cal.calendar_id === this.props.calendarId); - - if (selectedCalendar) { - formCalendarId = selectedCalendar.calendar_id; - eventsList = selectedCalendar.events; - - selectedCalendar.job_ids.forEach(id => { - if (jobIds.find(jobId => jobId === id)) { - selectedJobOptions.push({ label: id }); - } else if (groupIds.find(groupId => groupId === id)) { - selectedGroupOptions.push({ label: id }); - } - }); - } + async formSetup() { + try { + const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); + + const jobIdOptions = jobIds.map(jobId => ({ label: jobId })); + const groupIdOptions = groupIds.map(groupId => ({ label: groupId })); + + const selectedJobOptions = []; + const selectedGroupOptions = []; + let eventsList = []; + let selectedCalendar; + let formCalendarId = ''; + + // Editing existing calendar. + if (this.props.calendarId !== undefined) { + selectedCalendar = calendars.find(cal => cal.calendar_id === this.props.calendarId); + + if (selectedCalendar) { + formCalendarId = selectedCalendar.calendar_id; + eventsList = selectedCalendar.events; + + selectedCalendar.job_ids.forEach(id => { + if (jobIds.find(jobId => jobId === id)) { + selectedJobOptions.push({ label: id }); + } else if (groupIds.find(groupId => groupId === id)) { + selectedGroupOptions.push({ label: id }); + } + }); } - - this.setState({ - events: eventsList, - formCalendarId, - jobIds, - jobIdOptions, - groupIds, - groupIdOptions, - calendars, - loading: false, - selectedJobOptions, - selectedGroupOptions, - selectedCalendar, - }); - } catch (error) { - console.log(error); - this.setState({ loading: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.errorWithLoadingCalendarFromDataErrorMessage', - defaultMessage: - 'An error occurred loading calendar form data. Try refreshing the page.', - }) - ); } + + this.setState({ + events: eventsList, + formCalendarId, + jobIds, + jobIdOptions, + groupIds, + groupIdOptions, + calendars, + loading: false, + selectedJobOptions, + selectedGroupOptions, + selectedCalendar, + }); + } catch (error) { + console.log(error); + this.setState({ loading: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingCalendarFromDataErrorMessage', { + defaultMessage: 'An error occurred loading calendar form data. Try refreshing the page.', + }) + ); } + } - isDuplicateId = () => { - const { calendars, formCalendarId } = this.state; + isDuplicateId = () => { + const { calendars, formCalendarId } = this.state; - for (let i = 0; i < calendars.length; i++) { - if (calendars[i].calendar_id === formCalendarId) { - return true; - } + for (let i = 0; i < calendars.length; i++) { + if (calendars[i].calendar_id === formCalendarId) { + return true; } + } - return false; - }; + return false; + }; - onCreate = async () => { - const { formCalendarId } = this.state; - const { intl } = this.props; - - if (this.isDuplicateId()) { - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.canNotCreateCalendarWithExistingIdErrorMessag', - defaultMessage: - 'Cannot create calendar with id [{formCalendarId}] as it already exists.', - }, - { formCalendarId } - ) - ); - } else { - const calendar = this.setUpCalendarForApi(); - this.setState({ saving: true }); - - try { - await ml.addCalendar(calendar); - window.location = '#/settings/calendars_list'; - } catch (error) { - console.log('Error saving calendar', error); - this.setState({ saving: false }); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.errorWithCreatingCalendarErrorMessage', - defaultMessage: 'An error occurred creating calendar {calendarId}', - }, - { calendarId: calendar.calendarId } - ) - ); - } - } - }; + onCreate = async () => { + const { formCalendarId } = this.state; - onEdit = async () => { + if (this.isDuplicateId()) { + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.canNotCreateCalendarWithExistingIdErrorMessag', { + defaultMessage: 'Cannot create calendar with id [{formCalendarId}] as it already exists.', + values: { formCalendarId }, + }) + ); + } else { const calendar = this.setUpCalendarForApi(); this.setState({ saving: true }); try { - await ml.updateCalendar(calendar); + await ml.addCalendar(calendar); window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.errorWithUpdatingCalendarErrorMessage', - defaultMessage: - 'An error occurred saving calendar {calendarId}. Try refreshing the page.', - }, - { calendarId: calendar.calendarId } - ) + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithCreatingCalendarErrorMessage', { + defaultMessage: 'An error occurred creating calendar {calendarId}', + values: { calendarId: calendar.calendarId }, + }) ); } + } + }; + + onEdit = async () => { + const calendar = this.setUpCalendarForApi(); + this.setState({ saving: true }); + + try { + await ml.updateCalendar(calendar); + window.location = '#/settings/calendars_list'; + } catch (error) { + console.log('Error saving calendar', error); + this.setState({ saving: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithUpdatingCalendarErrorMessage', { + defaultMessage: + 'An error occurred saving calendar {calendarId}. Try refreshing the page.', + values: { calendarId: calendar.calendarId }, + }) + ); + } + }; + + setUpCalendarForApi = () => { + const { + formCalendarId, + description, + events, + selectedGroupOptions, + selectedJobOptions, + } = this.state; + + const jobIds = selectedJobOptions.map(option => option.label); + const groupIds = selectedGroupOptions.map(option => option.label); + + // Reduce events to fields expected by api + const eventsToSave = events.map(event => ({ + description: event.description, + start_time: event.start_time, + end_time: event.end_time, + })); + + // set up calendar + const calendar = { + calendarId: formCalendarId, + description, + events: eventsToSave, + job_ids: [...jobIds, ...groupIds], }; - setUpCalendarForApi = () => { - const { - formCalendarId, - description, - events, - selectedGroupOptions, - selectedJobOptions, - } = this.state; - - const jobIds = selectedJobOptions.map(option => option.label); - const groupIds = selectedGroupOptions.map(option => option.label); - - // Reduce events to fields expected by api - const eventsToSave = events.map(event => ({ - description: event.description, - start_time: event.start_time, - end_time: event.end_time, - })); - - // set up calendar - const calendar = { - calendarId: formCalendarId, - description, - events: eventsToSave, - job_ids: [...jobIds, ...groupIds], - }; - - return calendar; - }; - - onCreateGroupOption = newGroup => { - const newOption = { - label: newGroup, - }; - // Select the option. - this.setState(prevState => ({ - selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption), - })); - }; - - onJobSelection = selectedJobOptions => { - this.setState({ - selectedJobOptions, - }); - }; - - onGroupSelection = selectedGroupOptions => { - this.setState({ - selectedGroupOptions, - }); - }; - - onCalendarIdChange = e => { - const isValid = validateCalendarId(e.target.value); - - this.setState({ - formCalendarId: e.target.value, - isNewCalendarIdValid: isValid, - }); - }; - - onDescriptionChange = e => { - this.setState({ - description: e.target.value, - }); - }; - - showImportModal = () => { - this.setState(prevState => ({ - isImportModalVisible: !prevState.isImportModalVisible, - })); - }; - - closeImportModal = () => { - this.setState({ - isImportModalVisible: false, - }); - }; - - onEventDelete = eventId => { - this.setState(prevState => ({ - events: prevState.events.filter(event => event.event_id !== eventId), - })); - }; - - closeNewEventModal = () => { - this.setState({ isNewEventModalVisible: false }); - }; - - showNewEventModal = () => { - this.setState({ isNewEventModalVisible: true }); - }; - - addEvent = event => { - this.setState(prevState => ({ - events: [...prevState.events, event], - isNewEventModalVisible: false, - })); - }; + return calendar; + }; - addImportedEvents = events => { - this.setState(prevState => ({ - events: [...prevState.events, ...events], - isImportModalVisible: false, - })); + onCreateGroupOption = newGroup => { + const newOption = { + label: newGroup, }; - - render() { - const { - events, - isNewEventModalVisible, - isImportModalVisible, - isNewCalendarIdValid, - formCalendarId, - description, - groupIdOptions, - jobIdOptions, - saving, - selectedCalendar, - selectedJobOptions, - selectedGroupOptions, - } = this.state; - - let modal = ''; - - if (isNewEventModalVisible) { - modal = ( - - - - ); - } else if (isImportModalVisible) { - modal = ( - - - - ); - } - - return ( - - - - - - - - {modal} - - - + // Select the option. + this.setState(prevState => ({ + selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption), + })); + }; + + onJobSelection = selectedJobOptions => { + this.setState({ + selectedJobOptions, + }); + }; + + onGroupSelection = selectedGroupOptions => { + this.setState({ + selectedGroupOptions, + }); + }; + + onCalendarIdChange = e => { + const isValid = validateCalendarId(e.target.value); + + this.setState({ + formCalendarId: e.target.value, + isNewCalendarIdValid: isValid, + }); + }; + + onDescriptionChange = e => { + this.setState({ + description: e.target.value, + }); + }; + + showImportModal = () => { + this.setState(prevState => ({ + isImportModalVisible: !prevState.isImportModalVisible, + })); + }; + + closeImportModal = () => { + this.setState({ + isImportModalVisible: false, + }); + }; + + onEventDelete = eventId => { + this.setState(prevState => ({ + events: prevState.events.filter(event => event.event_id !== eventId), + })); + }; + + closeNewEventModal = () => { + this.setState({ isNewEventModalVisible: false }); + }; + + showNewEventModal = () => { + this.setState({ isNewEventModalVisible: true }); + }; + + addEvent = event => { + this.setState(prevState => ({ + events: [...prevState.events, event], + isNewEventModalVisible: false, + })); + }; + + addImportedEvents = events => { + this.setState(prevState => ({ + events: [...prevState.events, ...events], + isImportModalVisible: false, + })); + }; + + render() { + const { + events, + isNewEventModalVisible, + isImportModalVisible, + isNewCalendarIdValid, + formCalendarId, + description, + groupIdOptions, + jobIdOptions, + saving, + selectedCalendar, + selectedJobOptions, + selectedGroupOptions, + } = this.state; + + let modal = ''; + + if (isNewEventModalVisible) { + modal = ( + + + + ); + } else if (isImportModalVisible) { + modal = ( + + + ); } + + return ( + + + + + + + + {modal} + + + + ); } -); +} + +export const NewCalendar = withKibana(NewCalendarUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index e8999053a93bb..8dc174040f9c8 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -47,10 +47,9 @@ jest.mock('./utils', () => ({ }) ), })); -jest.mock('ui/timefilter', () => ({ - timefilter: { - disableTimeRangeSelector: jest.fn(), - disableAutoRefreshSelector: jest.fn(), +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; }, })); @@ -92,17 +91,31 @@ const calendars = [ const props = { canCreateCalendar: true, canDeleteCalendar: true, + kibana: { + services: { + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + }, + }, + }, + }, + }, + }, }; describe('NewCalendar', () => { test('Renders new calendar form', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Import modal shown on Import Events button click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-testid="ml_import_events"]'); const button = importButton.find('EuiButton'); @@ -112,7 +125,7 @@ describe('NewCalendar', () => { }); test('New event modal shown on New event button click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-testid="ml_new_event"]'); const button = importButton.find('EuiButton'); @@ -122,7 +135,7 @@ describe('NewCalendar', () => { }); test('isDuplicateId returns true if form calendar id already exists in calendars', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const instance = wrapper.instance(); instance.setState({ @@ -139,7 +152,7 @@ describe('NewCalendar', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-testid="ml_save_calendar_button"]'); const saveButton = buttons.find('EuiButton'); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 4efcf8e441c1e..814f30a70db54 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -27,290 +27,289 @@ import moment from 'moment'; import { TIME_FORMAT } from '../events_table'; import { generateTempId } from '../utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const VALID_DATE_STRING_LENGTH = 19; -export const NewEventModal = injectI18n( - class NewEventModal extends Component { - static propTypes = { - closeModal: PropTypes.func.isRequired, - addEvent: PropTypes.func.isRequired, +export class NewEventModal extends Component { + static propTypes = { + closeModal: PropTypes.func.isRequired, + addEvent: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + const startDate = moment().startOf('day'); + const endDate = moment() + .startOf('day') + .add(1, 'days'); + + this.state = { + startDate, + endDate, + description: '', + startDateString: startDate.format(TIME_FORMAT), + endDateString: endDate.format(TIME_FORMAT), }; + } - constructor(props) { - super(props); - - const startDate = moment().startOf('day'); - const endDate = moment() - .startOf('day') - .add(1, 'days'); - - this.state = { - startDate, - endDate, - description: '', - startDateString: startDate.format(TIME_FORMAT), - endDateString: endDate.format(TIME_FORMAT), - }; - } - - onDescriptionChange = e => { - this.setState({ - description: e.target.value, - }); + onDescriptionChange = e => { + this.setState({ + description: e.target.value, + }); + }; + + handleAddEvent = () => { + const { description, startDate, endDate } = this.state; + // Temp reference to unsaved events to allow removal from table + const tempId = generateTempId(); + + const event = { + description, + start_time: startDate.valueOf(), + end_time: endDate.valueOf(), + event_id: tempId, }; - handleAddEvent = () => { - const { description, startDate, endDate } = this.state; - // Temp reference to unsaved events to allow removal from table - const tempId = generateTempId(); - - const event = { - description, - start_time: startDate.valueOf(), - end_time: endDate.valueOf(), - event_id: tempId, - }; + this.props.addEvent(event); + }; - this.props.addEvent(event); - }; + handleChangeStart = date => { + let start = null; + let end = this.state.endDate; - handleChangeStart = date => { - let start = null; - let end = this.state.endDate; + const startMoment = moment(date); + const endMoment = moment(date); - const startMoment = moment(date); - const endMoment = moment(date); + start = startMoment.startOf('day'); - start = startMoment.startOf('day'); + if (start > end) { + end = endMoment.startOf('day').add(1, 'days'); + } + this.setState({ + startDate: start, + endDate: end, + startDateString: start.format(TIME_FORMAT), + endDateString: end.format(TIME_FORMAT), + }); + }; - if (start > end) { - end = endMoment.startOf('day').add(1, 'days'); - } - this.setState({ - startDate: start, - endDate: end, - startDateString: start.format(TIME_FORMAT), - endDateString: end.format(TIME_FORMAT), - }); - }; + handleChangeEnd = date => { + let start = this.state.startDate; + let end = null; - handleChangeEnd = date => { - let start = this.state.startDate; - let end = null; + const startMoment = moment(date); + const endMoment = moment(date); - const startMoment = moment(date); - const endMoment = moment(date); + end = endMoment.startOf('day'); - end = endMoment.startOf('day'); + if (start > end) { + start = startMoment.startOf('day').subtract(1, 'days'); + } + this.setState({ + startDate: start, + endDate: end, + startDateString: start.format(TIME_FORMAT), + endDateString: end.format(TIME_FORMAT), + }); + }; + + handleTimeStartChange = event => { + const dateString = event.target.value; + let isValidDate = false; + + if (dateString.length === VALID_DATE_STRING_LENGTH) { + isValidDate = moment(dateString).isValid(TIME_FORMAT, true); + } else { + this.setState({ + startDateString: dateString, + }); + } - if (start > end) { - start = startMoment.startOf('day').subtract(1, 'days'); - } + if (isValidDate) { this.setState({ - startDate: start, - endDate: end, - startDateString: start.format(TIME_FORMAT), - endDateString: end.format(TIME_FORMAT), + startDateString: dateString, + startDate: moment(dateString), }); - }; + } + }; - handleTimeStartChange = event => { - const dateString = event.target.value; - let isValidDate = false; - - if (dateString.length === VALID_DATE_STRING_LENGTH) { - isValidDate = moment(dateString).isValid(TIME_FORMAT, true); - } else { - this.setState({ - startDateString: dateString, - }); - } - - if (isValidDate) { - this.setState({ - startDateString: dateString, - startDate: moment(dateString), - }); - } - }; + handleTimeEndChange = event => { + const dateString = event.target.value; + let isValidDate = false; - handleTimeEndChange = event => { - const dateString = event.target.value; - let isValidDate = false; - - if (dateString.length === VALID_DATE_STRING_LENGTH) { - isValidDate = moment(dateString).isValid(TIME_FORMAT, true); - } else { - this.setState({ - endDateString: dateString, - }); - } - - if (isValidDate) { - this.setState({ - endDateString: dateString, - endDate: moment(dateString), - }); - } - }; + if (dateString.length === VALID_DATE_STRING_LENGTH) { + isValidDate = moment(dateString).isValid(TIME_FORMAT, true); + } else { + this.setState({ + endDateString: dateString, + }); + } - renderRangedDatePicker = () => { - const { startDate, endDate, startDateString, endDateString } = this.state; + if (isValidDate) { + this.setState({ + endDateString: dateString, + endDate: moment(dateString), + }); + } + }; - const { intl } = this.props; + renderRangedDatePicker = () => { + const { startDate, endDate, startDateString, endDateString } = this.state; - const timeInputs = ( - - - - - } - helpText={TIME_FORMAT} - > - + + + - - - + } + helpText={TIME_FORMAT} + > + + + + + + } + helpText={TIME_FORMAT} + > + + + + + + ); + + return ( + + + {timeInputs} + + + endDate} + aria-label={i18n.translate( + 'xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel', + { + defaultMessage: 'Start date', + } + )} + timeFormat={TIME_FORMAT} + dateFormat={TIME_FORMAT} + /> + } + endDateControl={ + endDate} + aria-label={i18n.translate( + 'xpack.ml.calendarsEdit.newEventModal.endDateAriaLabel', + { defaultMessage: 'End date' } + )} + timeFormat={TIME_FORMAT} + dateFormat={TIME_FORMAT} + /> + } + /> + + + ); + }; + + render() { + const { closeModal } = this.props; + const { description } = this.state; + + return ( + + + + + + + + + + } - helpText={TIME_FORMAT} + fullWidth > - - - - - ); - - return ( - - - {timeInputs} - - - endDate} - aria-label={intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel', - defaultMessage: 'Start date', - })} - timeFormat={TIME_FORMAT} - dateFormat={TIME_FORMAT} - /> - } - endDateControl={ - endDate} - aria-label={intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.newEventModal.endDateAriaLabel', - defaultMessage: 'End date', - })} - timeFormat={TIME_FORMAT} - dateFormat={TIME_FORMAT} /> - } - /> - - - ); - }; - - render() { - const { closeModal } = this.props; - const { description } = this.state; - - return ( - - - - - - - - - - - - } - fullWidth - > - - - - - - {this.renderRangedDatePicker()} - - + - - - - - - - - - - - ); - } + + + {this.renderRangedDatePicker()} + + + + + + + + + + + + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index bbb64584d8e1e..e91dce6124cef 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -21,14 +21,14 @@ const stateTimestamps = { describe('NewEventModal', () => { it('Add button disabled if description empty', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const addButton = wrapper.find('EuiButton').first(); expect(addButton.prop('disabled')).toBe(true); }); it('if endDate is less than startDate should set startDate one day before endDate', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const instance = wrapper.instance(); instance.setState({ startDate: moment(stateTimestamps.startDate), @@ -51,7 +51,7 @@ describe('NewEventModal', () => { }); it('if startDate is greater than endDate should set endDate one day after startDate', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const instance = wrapper.instance(); instance.setState({ startDate: moment(stateTimestamps.startDate), diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 867fd16932627..aeeeeef63a71e 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -14,11 +14,11 @@ exports[`CalendarsList Renders calendar list with calendars 1`] = ` horizontalPosition="center" verticalPosition="center" > - - { + this.setState({ loading: true }); - constructor(props) { - super(props); - this.state = { - loading: true, - calendars: [], + try { + const calendars = await ml.calendars(); + + this.setState({ + calendars, + loading: false, isDestroyModalVisible: false, - calendarId: null, - selectedForDeletion: [], - nodesAvailable: mlNodesAvailable(), - }; + }); + } catch (error) { + console.log(error); + this.setState({ loading: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage', { + defaultMessage: 'An error occurred loading the list of calendars.', + }) + ); } + }; - loadCalendars = async () => { - this.setState({ loading: true }); - - try { - const calendars = await ml.calendars(); - - this.setState({ - calendars, - loading: false, - isDestroyModalVisible: false, - }); - } catch (error) { - console.log(error); - this.setState({ loading: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage', - defaultMessage: 'An error occurred loading the list of calendars.', - }) - ); - } - }; + closeDestroyModal = () => { + this.setState({ isDestroyModalVisible: false, calendarId: null }); + }; - closeDestroyModal = () => { - this.setState({ isDestroyModalVisible: false, calendarId: null }); - }; + showDestroyModal = () => { + this.setState({ isDestroyModalVisible: true }); + }; - showDestroyModal = () => { - this.setState({ isDestroyModalVisible: true }); - }; + setSelectedCalendarList = selectedCalendars => { + this.setState({ selectedForDeletion: selectedCalendars }); + }; - setSelectedCalendarList = selectedCalendars => { - this.setState({ selectedForDeletion: selectedCalendars }); - }; + deleteCalendars = () => { + const { selectedForDeletion } = this.state; - deleteCalendars = () => { - const { selectedForDeletion } = this.state; + this.closeDestroyModal(); + deleteCalendars(selectedForDeletion, this.loadCalendars); + }; - this.closeDestroyModal(); - deleteCalendars(selectedForDeletion, this.loadCalendars); - }; + addRequiredFieldsToList = (calendarsList = []) => { + for (let i = 0; i < calendarsList.length; i++) { + calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', '); + calendarsList[i].events_length = calendarsList[i].events.length; + } - addRequiredFieldsToList = (calendarsList = []) => { - for (let i = 0; i < calendarsList.length; i++) { - calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', '); - calendarsList[i].events_length = calendarsList[i].events.length; - } + return calendarsList; + }; - return calendarsList; - }; + componentDidMount() { + this.loadCalendars(); + } - componentDidMount() { - this.loadCalendars(); + render() { + const { calendars, selectedForDeletion, loading, nodesAvailable } = this.state; + const { canCreateCalendar, canDeleteCalendar } = this.props; + let destroyModal = ''; + + if (this.state.isDestroyModalVisible) { + destroyModal = ( + + + } + onCancel={this.closeDestroyModal} + onConfirm={this.deleteCalendars} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

+ c.calendar_id).join(', '), + }} + /> +

+ + + ); } - render() { - const { calendars, selectedForDeletion, loading, nodesAvailable } = this.state; - const { canCreateCalendar, canDeleteCalendar } = this.props; - let destroyModal = ''; - - if (this.state.isDestroyModalVisible) { - destroyModal = ( - - - } - onCancel={this.closeDestroyModal} - onConfirm={this.deleteCalendars} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + return ( + + + + + -

- c.calendar_id).join(', '), - }} - /> -

-
-
- ); - } - - return ( - - - - - - - 0} - /> - - {destroyModal} - - - - ); - } + + 0} + /> + + {destroyModal} + + +
+ ); } -); +} + +export const CalendarsList = withKibana(CalendarsListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 5e4e2c1e0d31e..677703bceeca7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ml } from '../../../services/ml_api_service'; import { CalendarsList } from './calendars_list'; @@ -35,6 +35,17 @@ jest.mock('../../../services/ml_api_service', () => ({ }, })); +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: x => x }; +}); + +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + const testingState = { loading: false, calendars: [ @@ -76,34 +87,43 @@ const testingState = { const props = { canCreateCalendar: true, canDeleteCalendar: true, + kibana: { + services: { + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + }, + }, + }, + }, + notifications: { + toasts: { + addDanger: () => {}, + }, + }, + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; describe('CalendarsList', () => { test('loads calendars on mount', () => { ml.calendars = jest.fn(() => []); - shallowWithIntl(); + shallowWithIntl(); expect(ml.calendars).toHaveBeenCalled(); }); test('Renders calendar list with calendars', () => { - const wrapper = shallowWithIntl(); - + const wrapper = shallowWithIntl(); wrapper.instance().setState(testingState); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); - - test('Sets selected calendars list on checkbox change', () => { - const wrapper = mountWithIntl(); - - const instance = wrapper.instance(); - const spy = jest.spyOn(instance, 'setSelectedCalendarList'); - instance.setState(testingState); - wrapper.update(); - - const checkbox = wrapper.find('input[type="checkbox"]').first(); - checkbox.simulate('change'); - expect(spy).toHaveBeenCalled(); - }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index d1dbad0a85c06..f06812b2a9128 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; @@ -12,6 +12,7 @@ export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { return; } + const toastNotifications = getToastNotifications(); // Delete each of the specified calendars in turn, waiting for each response // before deleting the next to minimize load on the cluster. diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js index 58f0ac268fdb2..b97b918f03f74 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js @@ -23,12 +23,12 @@ import { EuiButtonEmpty, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-calendars.html`; +function CalendarsListHeaderUI({ totalCount, refreshCalendars, kibana }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = kibana.services.docLinks; -export function CalendarsListHeader({ totalCount, refreshCalendars }) { + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`; return ( @@ -99,7 +99,9 @@ export function CalendarsListHeader({ totalCount, refreshCalendars }) { ); } -CalendarsListHeader.propTypes = { +CalendarsListHeaderUI.propTypes = { totalCount: PropTypes.number.isRequired, refreshCalendars: PropTypes.func.isRequired, }; + +export const CalendarsListHeader = withKibana(CalendarsListHeaderUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js index 583c9fe7276ae..d0c3619f55919 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js @@ -9,12 +9,26 @@ import React from 'react'; import { CalendarsListHeader } from './header'; +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + describe('CalendarListsHeader', () => { const refreshCalendars = jest.fn(() => {}); const requiredProps = { totalCount: 3, refreshCalendars, + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; test('renders header', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index 774cc96517cc6..bd1dafcd6c0aa 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -9,9 +9,10 @@ import React from 'react'; import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -export const CalendarsListTable = injectI18n(function CalendarsListTable({ +export const CalendarsListTable = ({ calendarsList, onDeleteClick, setSelectedCalendarList, @@ -20,8 +21,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ canDeleteCalendar, mlNodesAvailable, itemsSelected, - intl, -}) { +}) => { const sorting = { sort: { field: 'calendar_id', @@ -37,8 +37,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ const columns = [ { field: 'calendar_id', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.idColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.idColumnName', { defaultMessage: 'ID', }), sortable: true, @@ -48,8 +47,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ }, { field: 'job_ids_string', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.jobsColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.jobsColumnName', { defaultMessage: 'Jobs', }), sortable: true, @@ -57,19 +55,15 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ }, { field: 'events_length', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.eventsColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.eventsColumnName', { defaultMessage: 'Events', }), sortable: true, render: eventsLength => - intl.formatMessage( - { - id: 'xpack.ml.calendarsList.table.eventsCountLabel', - defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', - }, - { eventsLength } - ), + i18n.translate('xpack.ml.calendarsList.table.eventsCountLabel', { + defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', + values: { eventsLength }, + }), }, ]; @@ -125,9 +119,9 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ /> ); -}); +}; -CalendarsListTable.WrappedComponent.propTypes = { +CalendarsListTable.propTypes = { calendarsList: PropTypes.array.isRequired, onDeleteClick: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js index 4d452309993a8..a4c5539d51d1b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js @@ -9,10 +9,6 @@ import React from 'react'; import { CalendarsListTable } from './table'; -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - const calendars = [ { calendar_id: 'farequote-calendar', @@ -41,12 +37,12 @@ const props = { describe('CalendarsListTable', () => { test('renders the table with all calendars', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('New button enabled if permission available', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -60,7 +56,7 @@ describe('CalendarsListTable', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -74,7 +70,7 @@ describe('CalendarsListTable', () => { mlNodesAvailable: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 68911d503966b..c6d1c239d3406 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../util/dependency_cache'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../services/ml_api_service'; @@ -13,6 +13,8 @@ export async function deleteFilterLists(filterListsToDelete) { return; } + const toastNotifications = getToastNotifications(); + // Delete each of the specified filter lists in turn, waiting for each response // before deleting the next to minimize load on the cluster. toastNotifications.add( diff --git a/x-pack/legacy/plugins/ml/public/application/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 index f91eec2ec996e..e1e32afe08dbe 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -13,100 +13,100 @@ import React, { Component } from 'react'; import { EuiButtonIcon, EuiPopover, EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -export const EditDescriptionPopover = injectI18n( - class extends Component { - static displayName = 'EditDescriptionPopover'; - static propTypes = { - description: PropTypes.string, - updateDescription: PropTypes.func.isRequired, - canCreateFilter: PropTypes.bool.isRequired, +export class EditDescriptionPopover extends Component { + static displayName = 'EditDescriptionPopover'; + static propTypes = { + description: PropTypes.string, + updateDescription: PropTypes.func.isRequired, + canCreateFilter: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + value: props.description, }; + } - constructor(props) { - super(props); + onChange = e => { + this.setState({ + value: e.target.value, + }); + }; - this.state = { - isPopoverOpen: false, - value: props.description, - }; + onButtonClick = () => { + if (this.state.isPopoverOpen === false) { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + value: this.props.description, + }); + } else { + this.closePopover(); } + }; - onChange = e => { + closePopover = () => { + if (this.state.isPopoverOpen === true) { this.setState({ - value: e.target.value, + isPopoverOpen: false, }); - }; - - onButtonClick = () => { - if (this.state.isPopoverOpen === false) { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - value: this.props.description, - }); - } else { - this.closePopover(); - } - }; - - closePopover = () => { - if (this.state.isPopoverOpen === true) { - this.setState({ - isPopoverOpen: false, - }); - this.props.updateDescription(this.state.value); - } - }; + this.props.updateDescription(this.state.value); + } + }; - render() { - const { isPopoverOpen, value } = this.state; - const { intl } = this.props; + render() { + const { isPopoverOpen, value } = this.state; - const button = ( - - ); + } + )} + isDisabled={this.props.canCreateFilter === false} + /> + ); - return ( -
- -
- - - } - > - + +
+ + - - -
-
-
- ); - } + } + > + + + +
+ +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 43234dbc7bdc7..f97bfe6682f5e 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -16,7 +16,7 @@ function prepareTest(updateDescriptionFn) { canCreateFilter: true, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); return wrapper; } @@ -30,7 +30,7 @@ describe('FilterListUsagePopover', () => { canCreateFilter: true, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 85a31fbcd9185..074654dc754fc 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -93,7 +93,7 @@ exports[`EditFilterListHeader renders the header when creating a new filter list - - @@ -300,7 +300,7 @@ exports[`EditFilterListHeader renders the header when editing an existing unused - @@ -397,7 +397,7 @@ exports[`EditFilterListHeader renders the header when editing an existing used f - { - const { intl } = this.props; - - ml.filters - .filters({ filterId }) - .then(filter => { - this.setLoadedFilterState(filter); - }) - .catch(resp => { - console.log(`Error loading filter ${filterId}:`, resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.settings.filterLists.editFilterList.loadingDetailsOfFilterErrorMessage', - defaultMessage: 'An error occurred loading details of filter {filterId}', - }, - { + loadFilterList = filterId => { + ml.filters + .filters({ filterId }) + .then(filter => { + this.setLoadedFilterState(filter); + }) + .catch(resp => { + console.log(`Error loading filter ${filterId}:`, resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.settings.filterLists.editFilterList.loadingDetailsOfFilterErrorMessage', + { + defaultMessage: 'An error occurred loading details of filter {filterId}', + values: { filterId, - } - ) - ); - }); - }; - - setLoadedFilterState = loadedFilter => { - // Store the loaded filter so we can diff changes to the items when saving updates. - this.setState(prevState => { - const { itemsPerPage, searchQuery } = prevState; - - const matchingItems = getMatchingFilterItems(searchQuery, loadedFilter.items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - description: loadedFilter.description, - items: loadedFilter.items !== undefined ? [...loadedFilter.items] : [], - matchingItems, - selectedItems: [], - loadedFilter, - isNewFilterIdInvalid: false, - activePage, - searchQuery, - saveInProgress: false, - }; + }, + } + ) + ); }); - }; + }; - updateNewFilterId = newFilterId => { - this.setState({ - newFilterId, - isNewFilterIdInvalid: !isValidFilterListId(newFilterId), - }); - }; + setLoadedFilterState = loadedFilter => { + // Store the loaded filter so we can diff changes to the items when saving updates. + this.setState(prevState => { + const { itemsPerPage, searchQuery } = prevState; - updateDescription = description => { - this.setState({ description }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, loadedFilter.items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - addItems = itemsToAdd => { - const { intl } = this.props; - - this.setState(prevState => { - const { itemsPerPage, searchQuery } = prevState; - const items = [...prevState.items]; - const alreadyInFilter = []; - itemsToAdd.forEach(item => { - if (items.indexOf(item) === -1) { - items.push(item); - } else { - alreadyInFilter.push(item); - } - }); - items.sort((str1, str2) => { - return str1.localeCompare(str2); - }); - - if (alreadyInFilter.length > 0) { - toastNotifications.addWarning( - intl.formatMessage( - { - id: - 'xpack.ml.settings.filterLists.editFilterList.duplicatedItemsInFilterListWarningMessage', - defaultMessage: - 'The following items were already in the filter list: {alreadyInFilter}', - }, - { - alreadyInFilter, - } - ) - ); + return { + description: loadedFilter.description, + items: loadedFilter.items !== undefined ? [...loadedFilter.items] : [], + matchingItems, + selectedItems: [], + loadedFilter, + isNewFilterIdInvalid: false, + activePage, + searchQuery, + saveInProgress: false, + }; + }); + }; + + updateNewFilterId = newFilterId => { + this.setState({ + newFilterId, + isNewFilterIdInvalid: !isValidFilterListId(newFilterId), + }); + }; + + updateDescription = description => { + this.setState({ description }); + }; + + addItems = itemsToAdd => { + this.setState(prevState => { + const { itemsPerPage, searchQuery } = prevState; + const items = [...prevState.items]; + const alreadyInFilter = []; + itemsToAdd.forEach(item => { + if (items.indexOf(item) === -1) { + items.push(item); + } else { + alreadyInFilter.push(item); } - - const matchingItems = getMatchingFilterItems(searchQuery, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - items, - matchingItems, - activePage, - searchQuery, - }; }); - }; - - deleteSelectedItems = () => { - this.setState(prevState => { - const { selectedItems, itemsPerPage, searchQuery } = prevState; - const items = [...prevState.items]; - selectedItems.forEach(item => { - const index = items.indexOf(item); - if (index !== -1) { - items.splice(index, 1); - } - }); - - const matchingItems = getMatchingFilterItems(searchQuery, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - items, - matchingItems, - selectedItems: [], - activePage, - searchQuery, - }; + items.sort((str1, str2) => { + return str1.localeCompare(str2); }); - }; - onSearchChange = ({ query }) => { - this.setState(prevState => { - const { items, itemsPerPage } = prevState; - - const matchingItems = getMatchingFilterItems(query, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); + if (alreadyInFilter.length > 0) { + const { toasts } = this.props.kibana.services.notifications; + toasts.addWarning( + i18n.translate( + 'xpack.ml.settings.filterLists.editFilterList.duplicatedItemsInFilterListWarningMessage', + { + defaultMessage: + 'The following items were already in the filter list: {alreadyInFilter}', + values: { + alreadyInFilter, + }, + } + ) + ); + } - return { - matchingItems, - activePage, - searchQuery: query, - }; - }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - setItemSelected = (item, isSelected) => { - this.setState(prevState => { - const selectedItems = [...prevState.selectedItems]; - const index = selectedItems.indexOf(item); - if (isSelected === true && index === -1) { - selectedItems.push(item); - } else if (isSelected === false && index !== -1) { - selectedItems.splice(index, 1); + return { + items, + matchingItems, + activePage, + searchQuery, + }; + }); + }; + + deleteSelectedItems = () => { + this.setState(prevState => { + const { selectedItems, itemsPerPage, searchQuery } = prevState; + const items = [...prevState.items]; + selectedItems.forEach(item => { + const index = items.indexOf(item); + if (index !== -1) { + items.splice(index, 1); } - - return { - selectedItems, - }; }); - }; - setActivePage = activePage => { - this.setState({ activePage }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - setItemsPerPage = itemsPerPage => { - this.setState({ - itemsPerPage, - activePage: 0, - }); - }; + return { + items, + matchingItems, + selectedItems: [], + activePage, + searchQuery, + }; + }); + }; - save = () => { - this.setState({ saveInProgress: true }); - - const { loadedFilter, newFilterId, description, items } = this.state; - const { intl } = this.props; - const filterId = this.props.filterId !== undefined ? this.props.filterId : newFilterId; - saveFilterList(filterId, description, items, loadedFilter) - .then(savedFilter => { - this.setLoadedFilterState(savedFilter); - returnToFiltersList(); - }) - .catch(resp => { - console.log(`Error saving filter ${filterId}:`, resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.settings.filterLists.editFilterList.savingFilterErrorMessage', - defaultMessage: 'An error occurred saving filter {filterId}', - }, - { - filterId, - } - ) - ); - this.setState({ saveInProgress: false }); - }); - }; + onSearchChange = ({ query }) => { + this.setState(prevState => { + const { items, itemsPerPage } = prevState; - render() { - const { - loadedFilter, - newFilterId, - isNewFilterIdInvalid, - description, - items, + const matchingItems = getMatchingFilterItems(query, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); + + return { matchingItems, - selectedItems, - itemsPerPage, activePage, - saveInProgress, - } = this.state; - const { canCreateFilter, canDeleteFilter } = this.props; - - const totalItemCount = items !== undefined ? items.length : 0; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } + searchQuery: query, + }; + }); + }; + + setItemSelected = (item, isSelected) => { + this.setState(prevState => { + const selectedItems = [...prevState.selectedItems]; + const index = selectedItems.indexOf(item); + if (isSelected === true && index === -1) { + selectedItems.push(item); + } else if (isSelected === false && index !== -1) { + selectedItems.splice(index, 1); + } + + return { + selectedItems, + }; + }); + }; + + setActivePage = activePage => { + this.setState({ activePage }); + }; + + setItemsPerPage = itemsPerPage => { + this.setState({ + itemsPerPage, + activePage: 0, + }); + }; + + save = () => { + this.setState({ saveInProgress: true }); + + const { loadedFilter, newFilterId, description, items } = this.state; + const filterId = this.props.filterId !== undefined ? this.props.filterId : newFilterId; + saveFilterList(filterId, description, items, loadedFilter) + .then(savedFilter => { + this.setLoadedFilterState(savedFilter); + returnToFiltersList(); + }) + .catch(resp => { + console.log(`Error saving filter ${filterId}:`, resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.settings.filterLists.editFilterList.savingFilterErrorMessage', { + defaultMessage: 'An error occurred saving filter {filterId}', + values: { + filterId, + }, + }) + ); + this.setState({ saveInProgress: false }); + }); + }; + + render() { + const { + loadedFilter, + newFilterId, + isNewFilterIdInvalid, + description, + items, + matchingItems, + selectedItems, + itemsPerPage, + activePage, + saveInProgress, + } = this.state; + const { canCreateFilter, canDeleteFilter } = this.props; + + const totalItemCount = items !== undefined ? items.length : 0; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); } -); +} +export const EditFilterList = withKibana(EditFilterListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 6ca29ab3f35f2..508fd7972da00 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -36,6 +36,12 @@ jest.mock('../../../services/ml_api_service', () => ({ }, })); +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -47,7 +53,7 @@ const props = { }; function prepareEditTest() { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters // has resolved. @@ -62,7 +68,7 @@ function prepareEditTest() { describe('EditFilterList', () => { test('renders the edit page for a new filter list and updates ID', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); const instance = wrapper.instance(); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js index 86a2235fcfef0..f1efa173178f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js @@ -23,12 +23,13 @@ import { EuiTitle, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EditDescriptionPopover } from '../components/edit_description_popover'; import { FilterListUsagePopover } from '../components/filter_list_usage_popover'; -export const EditFilterListHeader = injectI18n(function({ +export const EditFilterListHeader = ({ canCreateFilter, filterId, totalItemCount, @@ -38,8 +39,7 @@ export const EditFilterListHeader = injectI18n(function({ isNewFilterIdInvalid, updateNewFilterId, usedBy, - intl, -}) { +}) => { const title = filterId !== undefined ? ( ); -}); +}; -EditFilterListHeader.WrappedComponent.propTypes = { +EditFilterListHeader.propTypes = { canCreateFilter: PropTypes.bool.isRequired, filterId: PropTypes.string, newFilterId: PropTypes.string, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js index acd2ed88cbecc..b23b1eedf172a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js @@ -28,7 +28,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -42,7 +42,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 15, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -54,7 +54,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -71,7 +71,7 @@ describe('EditFilterListHeader', () => { }, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); 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 index 1995b66c23326..c82be4cbfa71e 100644 --- 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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { isJobIdValid } from '../../../../../common/util/job_utils'; import { ml } from '../../../services/ml_api_service'; @@ -68,6 +68,7 @@ export function addFilterList(filterId, description, items) { reject(error); }); } else { + const toastNotifications = getToastNotifications(); toastNotifications.addDanger(filterWithIdExistsErrorMessage); reject(new Error(filterWithIdExistsErrorMessage)); } diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 52971bfe49cd9..5f0cc22fce8b0 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -14,7 +14,7 @@ exports[`Filter Lists renders a list of filters 1`] = ` horizontalPosition="center" verticalPosition="center" > - diff --git a/x-pack/legacy/plugins/ml/public/application/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 index 77936b16667b1..ee9014f752b0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/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 @@ -1,112 +1,127 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Filter Lists Header renders header 1`] = ` - - - - - - -

- -

-
-
- - -

- -

-
-
-
-
- - - - - - - - - -
- - -

- - , - "learnMoreLink": - - , - } - } - /> - -

-
- -
+ `; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js index 949dfe82d9f54..90c65adaaef02 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js @@ -13,106 +13,106 @@ import { PropTypes } from 'prop-types'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; - -import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; import { NavigationMenu } from '../../../components/navigation_menu'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { FilterListsHeader } from './header'; import { FilterListsTable } from './table'; import { ml } from '../../../services/ml_api_service'; -export const FilterLists = injectI18n( - class extends Component { - static displayName = 'FilterLists'; - static propTypes = { - canCreateFilter: PropTypes.bool.isRequired, - canDeleteFilter: PropTypes.bool.isRequired, - }; +export class FilterListsUI extends Component { + static displayName = 'FilterLists'; + static propTypes = { + canCreateFilter: PropTypes.bool.isRequired, + canDeleteFilter: PropTypes.bool.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - filterLists: [], - selectedFilterLists: [], - }; - } - - componentDidMount() { - this.refreshFilterLists(); - } - - setFilterLists = filterLists => { - // Check selected filter lists still exist. - this.setState(prevState => { - const loadedFilterIds = filterLists.map(filterList => filterList.filter_id); - const selectedFilterLists = prevState.selectedFilterLists.filter(filterList => { - return loadedFilterIds.indexOf(filterList.filter_id) !== -1; - }); - - return { - filterLists, - selectedFilterLists, - }; - }); + this.state = { + filterLists: [], + selectedFilterLists: [], }; + } - setSelectedFilterLists = selectedFilterLists => { - this.setState({ selectedFilterLists }); - }; + componentDidMount() { + this.refreshFilterLists(); + } - refreshFilterLists = () => { - const { intl } = this.props; - // Load the list of filters. - ml.filters - .filtersStats() - .then(filterLists => { - this.setFilterLists(filterLists); - }) - .catch(resp => { - console.log('Error loading list of filters:', resp); - toastNotifications.addDanger( - intl.formatMessage({ - id: 'xpack.ml.settings.filterLists.filterLists.loadingFilterListsErrorMessage', - defaultMessage: 'An error occurred loading the filter lists', - }) - ); - }); - }; + setFilterLists = filterLists => { + // Check selected filter lists still exist. + this.setState(prevState => { + const loadedFilterIds = filterLists.map(filterList => filterList.filter_id); + const selectedFilterLists = prevState.selectedFilterLists.filter(filterList => { + return loadedFilterIds.indexOf(filterList.filter_id) !== -1; + }); - render() { - const { filterLists, selectedFilterLists } = this.state; - const { canCreateFilter, canDeleteFilter } = this.props; - - return ( - - - - - - - - - - - - ); - } + return { + filterLists, + selectedFilterLists, + }; + }); + }; + + setSelectedFilterLists = selectedFilterLists => { + this.setState({ selectedFilterLists }); + }; + + refreshFilterLists = () => { + // Load the list of filters. + ml.filters + .filtersStats() + .then(filterLists => { + this.setFilterLists(filterLists); + }) + .catch(resp => { + console.log('Error loading list of filters:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.settings.filterLists.filterLists.loadingFilterListsErrorMessage', + { + defaultMessage: 'An error occurred loading the filter lists', + } + ) + ); + }); + }; + + render() { + const { filterLists, selectedFilterLists } = this.state; + const { canCreateFilter, canDeleteFilter } = this.props; + + return ( + + + + + + + + + + + + ); } -); +} +export const FilterLists = withKibana(FilterListsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index b7be6f1954066..ac9b6e8eb8e7f 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -16,6 +16,12 @@ jest.mock('../../../privilege/check_privilege', () => ({ checkPermission: () => true, })); +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + // Mock the call for loading the list of filters. // The mock is hoisted to the top, so need to prefix the filter variable // with 'mock' so it can be used lazily. @@ -42,7 +48,7 @@ const props = { describe('Filter Lists', () => { test('renders a list of filters', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters // has resolved. diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js index ae0c2ef4338ec..b6ad0e0aec49d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js @@ -23,12 +23,11 @@ import { EuiButtonEmpty, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-rules.html`; - -export function FilterListsHeader({ totalCount, refreshFilterLists }) { +function FilterListsHeaderUI({ totalCount, refreshFilterLists, kibana }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`; return ( @@ -99,7 +98,9 @@ You can use the same filter list in multiple jobs.{br}{learnMoreLink}" ); } -FilterListsHeader.propTypes = { +FilterListsHeaderUI.propTypes = { totalCount: PropTypes.number.isRequired, refreshFilterLists: PropTypes.func.isRequired, }; + +export const FilterListsHeader = withKibana(FilterListsHeaderUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js index 0d1ca66de5775..fcbf90ec62d4a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -22,12 +22,12 @@ import { EuiText, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DeleteFilterListModal } from '../components/delete_filter_list_modal'; -const UsedByIcon = injectI18n(function({ usedBy, intl }) { +function UsedByIcon({ usedBy }) { // Renders a tick or cross in the 'usedBy' column to indicate whether // the filter list is in use in a detectors in any jobs. let icon; @@ -35,8 +35,7 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { icon = ( @@ -45,8 +44,7 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { icon = ( @@ -54,9 +52,9 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { } return icon; -}); +} -UsedByIcon.WrappedComponent.propTypes = { +UsedByIcon.propTypes = { usedBy: PropTypes.object, }; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js index 8efe558fda961..6b4e752845774 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js @@ -9,7 +9,6 @@ import React from 'react'; import { Settings } from './settings'; -jest.mock('../contexts/ui/use_ui_chrome_context'); jest.mock('../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 9aafab12a7156..2084998136460 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -15,8 +15,6 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -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'; @@ -28,7 +26,9 @@ import { PROGRESS_STATES } from './progress_states'; 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'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. @@ -54,483 +54,486 @@ function getDefaultState() { }; } -export const ForecastingModal = injectI18n( - class ForecastingModal extends Component { - static propTypes = { - isDisabled: PropTypes.bool, - job: PropTypes.object, - detectorIndex: PropTypes.number, - entities: PropTypes.array, - setForecastId: PropTypes.func, - }; - - constructor(props) { - super(props); - this.state = getDefaultState(); - - // Used to poll for updates on a running forecast. - this.forecastChecker = null; - } +export class ForecastingModalUI extends Component { + static propTypes = { + isDisabled: PropTypes.bool, + job: PropTypes.object, + detectorIndex: PropTypes.number, + entities: PropTypes.array, + setForecastId: PropTypes.func, + }; + + constructor(props) { + super(props); + this.state = getDefaultState(); + + // Used to poll for updates on a running forecast. + this.forecastChecker = null; + } - addMessage = (message, status, clearFirst = false) => { - const msg = { message, status }; - - this.setState(prevState => ({ - messages: clearFirst ? [msg] : [...prevState.messages, msg], - })); - }; - - viewForecast = forecastId => { - this.props.setForecastId(forecastId); - this.closeModal(); - }; - - onNewForecastDurationChange = event => { - const { intl } = this.props; - const newForecastDurationErrors = []; - let isNewForecastDurationValid = true; - const duration = parseInterval(event.target.value); - if (duration === null) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.forecastingModal.invalidDurationFormatErrorMessage', + addMessage = (message, status, clearFirst = false) => { + const msg = { message, status }; + + this.setState(prevState => ({ + messages: clearFirst ? [msg] : [...prevState.messages, msg], + })); + }; + + viewForecast = forecastId => { + this.props.setForecastId(forecastId); + this.closeModal(); + }; + + onNewForecastDurationChange = event => { + const newForecastDurationErrors = []; + let isNewForecastDurationValid = true; + const duration = parseInterval(event.target.value); + if (duration === null) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.invalidDurationFormatErrorMessage', + { defaultMessage: 'Invalid duration format', - }) - ); - } else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage', - defaultMessage: - 'Forecast duration must not be greater than {maximumForecastDurationDays} days', - }, - { maximumForecastDurationDays: FORECAST_DURATION_MAX_DAYS } - ) - ); - } else if (duration.asMilliseconds() === 0) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeZeroErrorMessage', + } + ) + ); + } else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage', + { + defaultMessage: + 'Forecast duration must not be greater than {maximumForecastDurationDays} days', + values: { maximumForecastDurationDays: FORECAST_DURATION_MAX_DAYS }, + } + ) + ); + } else if (duration.asMilliseconds() === 0) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeZeroErrorMessage', + { defaultMessage: 'Forecast duration must not be zero', - }) - ); - } - - this.setState({ - newForecastDuration: event.target.value, - isNewForecastDurationValid, - newForecastDurationErrors, - }); - }; + } + ) + ); + } - checkJobStateAndRunForecast = () => { - this.setState({ - isForecastRequested: true, - messages: [], - }); + this.setState({ + newForecastDuration: event.target.value, + isNewForecastDurationValid, + newForecastDurationErrors, + }); + }; - // A forecast can only be run on an opened job, - // so open job if it is closed. - if (this.props.job.state === JOB_STATE.CLOSED) { - this.openJobAndRunForecast(); - } else { - this.runForecast(false); - } - }; + checkJobStateAndRunForecast = () => { + this.setState({ + isForecastRequested: true, + messages: [], + }); + + // A forecast can only be run on an opened job, + // so open job if it is closed. + if (this.props.job.state === JOB_STATE.CLOSED) { + this.openJobAndRunForecast(); + } else { + this.runForecast(false); + } + }; - openJobAndRunForecast = () => { - // Opens a job in a 'closed' state prior to running a forecast. - this.setState({ - jobOpeningState: PROGRESS_STATES.WAITING, + openJobAndRunForecast = () => { + // Opens a job in a 'closed' state prior to running a forecast. + this.setState({ + jobOpeningState: PROGRESS_STATES.WAITING, + }); + + mlJobService + .openJob(this.props.job.job_id) + .then(() => { + // If open was successful run the forecast, then close the job again. + this.setState({ + jobOpeningState: PROGRESS_STATES.DONE, + }); + this.runForecast(true); + }) + .catch(resp => { + console.log('Time series forecast modal - could not open job:', resp); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithOpeningJobBeforeRunningForecastErrorMessage', + { + defaultMessage: 'Error opening job before running forecast', + } + ), + MESSAGE_LEVEL.ERROR + ); + this.setState({ + jobOpeningState: PROGRESS_STATES.ERROR, + }); }); + }; + + runForecastErrorHandler = (resp, closeJob) => { + this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); + console.log('Time series forecast modal - error running forecast:', resp); + if (resp && resp.message) { + this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true); + } else { + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage', + { + defaultMessage: + 'Unexpected response from running forecast. The request may have failed.', + } + ), + MESSAGE_LEVEL.ERROR, + true + ); + } + if (closeJob === true) { + this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); mlJobService - .openJob(this.props.job.job_id) + .closeJob(this.props.job.job_id) .then(() => { - // If open was successful run the forecast, then close the job again. - this.setState({ - jobOpeningState: PROGRESS_STATES.DONE, - }); - this.runForecast(true); + this.setState({ jobClosingState: PROGRESS_STATES.DONE }); }) - .catch(resp => { - console.log('Time series forecast modal - could not open job:', resp); + .catch(response => { + console.log('Time series forecast modal - could not close job:', response); this.addMessage( - this.props.intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithOpeningJobBeforeRunningForecastErrorMessage', - defaultMessage: 'Error opening job before running forecast', - }), + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobErrorMessage', + { + defaultMessage: 'Error closing job', + } + ), MESSAGE_LEVEL.ERROR ); - this.setState({ - jobOpeningState: PROGRESS_STATES.ERROR, - }); + this.setState({ jobClosingState: PROGRESS_STATES.ERROR }); }); - }; - - runForecastErrorHandler = (resp, closeJob) => { - const intl = this.props.intl; - - this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); - console.log('Time series forecast modal - error running forecast:', resp); - if (resp && resp.message) { - this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true); - } else { - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage', - defaultMessage: - 'Unexpected response from running forecast. The request may have failed.', - }), - MESSAGE_LEVEL.ERROR, - true - ); - } - - if (closeJob === true) { - this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); - mlJobService - .closeJob(this.props.job.job_id) - .then(() => { - this.setState({ jobClosingState: PROGRESS_STATES.DONE }); - }) - .catch(response => { - console.log('Time series forecast modal - could not close job:', response); - this.addMessage( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobErrorMessage', - defaultMessage: 'Error closing job', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ jobClosingState: PROGRESS_STATES.ERROR }); - }); - } - }; - - runForecast = closeJobAfterRunning => { - this.setState({ - forecastProgress: 0, - }); + } + }; - // Always supply the duration to the endpoint in seconds as some of the moment duration - // formats accepted by Kibana (w, M, y) are not valid formats in Elasticsearch. - const durationInSeconds = parseInterval(this.state.newForecastDuration).asSeconds(); + runForecast = closeJobAfterRunning => { + this.setState({ + forecastProgress: 0, + }); + + // Always supply the duration to the endpoint in seconds as some of the moment duration + // formats accepted by Kibana (w, M, y) are not valid formats in Elasticsearch. + const durationInSeconds = parseInterval(this.state.newForecastDuration).asSeconds(); + + mlForecastService + .runForecast(this.props.job.job_id, `${durationInSeconds}s`) + .then(resp => { + // Endpoint will return { acknowledged:true, id: } before forecast is complete. + // So wait for results and then refresh the dashboard to the end of the forecast. + if (resp.forecast_id !== undefined) { + this.waitForForecastResults(resp.forecast_id, closeJobAfterRunning); + } else { + this.runForecastErrorHandler(resp, closeJobAfterRunning); + } + }) + .catch(resp => this.runForecastErrorHandler(resp, closeJobAfterRunning)); + }; + waitForForecastResults = (forecastId, closeJobAfterRunning) => { + // Obtain the stats for the forecast request and check forecast is progressing. + // When the stats show the forecast is finished, load the + // forecast results into the view. + let previousProgress = 0; + let noProgressMs = 0; + this.forecastChecker = setInterval(() => { mlForecastService - .runForecast(this.props.job.job_id, `${durationInSeconds}s`) + .getForecastRequestStats(this.props.job, forecastId) .then(resp => { - // Endpoint will return { acknowledged:true, id: } before forecast is complete. - // So wait for results and then refresh the dashboard to the end of the forecast. - if (resp.forecast_id !== undefined) { - this.waitForForecastResults(resp.forecast_id, closeJobAfterRunning); - } else { - this.runForecastErrorHandler(resp, closeJobAfterRunning); + // Get the progress (stats value is between 0 and 1). + const progress = _.get(resp, ['stats', 'forecast_progress'], previousProgress); + const status = _.get(resp, ['stats', 'forecast_status']); + + // The requests for forecast stats can get routed to different shards, + // and if these operate at different speeds there is a chance that a + // previous request could arrive later. + // The progress reported by the back-end should never go down, so + // to be on the safe side, only update state if progress has increased. + if (progress > previousProgress) { + this.setState({ forecastProgress: Math.round(100 * progress) }); } - }) - .catch(resp => this.runForecastErrorHandler(resp, closeJobAfterRunning)); - }; - - waitForForecastResults = (forecastId, closeJobAfterRunning) => { - // Obtain the stats for the forecast request and check forecast is progressing. - // When the stats show the forecast is finished, load the - // forecast results into the view. - const { intl } = this.props; - let previousProgress = 0; - let noProgressMs = 0; - this.forecastChecker = setInterval(() => { - mlForecastService - .getForecastRequestStats(this.props.job, forecastId) - .then(resp => { - // Get the progress (stats value is between 0 and 1). - const progress = _.get(resp, ['stats', 'forecast_progress'], previousProgress); - const status = _.get(resp, ['stats', 'forecast_status']); - - // The requests for forecast stats can get routed to different shards, - // and if these operate at different speeds there is a chance that a - // previous request could arrive later. - // The progress reported by the back-end should never go down, so - // to be on the safe side, only update state if progress has increased. - if (progress > previousProgress) { - this.setState({ forecastProgress: Math.round(100 * progress) }); - } - // Display any messages returned in the request stats. - let messages = _.get(resp, ['stats', 'forecast_messages'], []); - messages = messages.map(message => ({ message, status: MESSAGE_LEVEL.WARNING })); - this.setState({ messages }); - - if (status === FORECAST_REQUEST_STATE.FINISHED) { - clearInterval(this.forecastChecker); - - if (closeJobAfterRunning === true) { - this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); - mlJobService - .closeJob(this.props.job.job_id) - .then(() => { - this.setState({ - jobClosingState: PROGRESS_STATES.DONE, - }); - this.props.setForecastId(forecastId); - this.closeAfterRunningForecast(); - }) - .catch(response => { - // Load the forecast data in the main page, - // but leave this dialog open so the error can be viewed. - console.log('Time series forecast modal - could not close job:', response); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobAfterRunningForecastErrorMessage', - defaultMessage: 'Error closing job after running forecast', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ - jobClosingState: PROGRESS_STATES.ERROR, - }); - this.props.setForecastId(forecastId); + // Display any messages returned in the request stats. + let messages = _.get(resp, ['stats', 'forecast_messages'], []); + messages = messages.map(message => ({ message, status: MESSAGE_LEVEL.WARNING })); + this.setState({ messages }); + + if (status === FORECAST_REQUEST_STATE.FINISHED) { + clearInterval(this.forecastChecker); + + if (closeJobAfterRunning === true) { + this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); + mlJobService + .closeJob(this.props.job.job_id) + .then(() => { + this.setState({ + jobClosingState: PROGRESS_STATES.DONE, }); - } else { - this.props.setForecastId(forecastId); - this.closeAfterRunningForecast(); - } - } else { - // Display a warning and abort check if the forecast hasn't - // progressed for WARN_NO_PROGRESS_MS. - if (progress === previousProgress) { - noProgressMs += FORECAST_STATS_POLL_FREQUENCY; - if (noProgressMs > WARN_NO_PROGRESS_MS) { - console.log( - `Forecast request has not progressed for ${WARN_NO_PROGRESS_MS}ms. Cancelling check.` - ); + this.props.setForecastId(forecastId); + this.closeAfterRunningForecast(); + }) + .catch(response => { + // Load the forecast data in the main page, + // but leave this dialog open so the error can be viewed. + console.log('Time series forecast modal - could not close job:', response); this.addMessage( - intl.formatMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobAfterRunningForecastErrorMessage', { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.noProgressReportedForNewForecastErrorMessage', - defaultMessage: - 'No progress reported for the new forecast for {WarnNoProgressMs}ms.' + - 'An error may have occurred whilst running the forecast.', - }, - { WarnNoProgressMs: WARN_NO_PROGRESS_MS } + defaultMessage: 'Error closing job after running forecast', + } ), MESSAGE_LEVEL.ERROR ); - - // Try and load any results which may have been created. + this.setState({ + jobClosingState: PROGRESS_STATES.ERROR, + }); this.props.setForecastId(forecastId); - this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); - clearInterval(this.forecastChecker); - } - } else { - if (progress > previousProgress) { - previousProgress = progress; - } - - // Reset the 'no progress' check value. - noProgressMs = 0; + }); + } else { + this.props.setForecastId(forecastId); + this.closeAfterRunningForecast(); + } + } else { + // Display a warning and abort check if the forecast hasn't + // progressed for WARN_NO_PROGRESS_MS. + if (progress === previousProgress) { + noProgressMs += FORECAST_STATS_POLL_FREQUENCY; + if (noProgressMs > WARN_NO_PROGRESS_MS) { + console.log( + `Forecast request has not progressed for ${WARN_NO_PROGRESS_MS}ms. Cancelling check.` + ); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.noProgressReportedForNewForecastErrorMessage', + { + defaultMessage: + 'No progress reported for the new forecast for {WarnNoProgressMs}ms.' + + 'An error may have occurred whilst running the forecast.', + values: { WarnNoProgressMs: WARN_NO_PROGRESS_MS }, + } + ), + MESSAGE_LEVEL.ERROR + ); + + // Try and load any results which may have been created. + this.props.setForecastId(forecastId); + this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); + clearInterval(this.forecastChecker); + } + } else { + if (progress > previousProgress) { + previousProgress = progress; } + + // Reset the 'no progress' check value. + noProgressMs = 0; } - }) - .catch(resp => { - console.log( - 'Time series forecast modal - error loading stats of forecast from elasticsearch:', - resp - ); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithLoadingStatsOfRunningForecastErrorMessage', + } + }) + .catch(resp => { + console.log( + 'Time series forecast modal - error loading stats of forecast from elasticsearch:', + resp + ); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithLoadingStatsOfRunningForecastErrorMessage', + { defaultMessage: 'Error loading stats of running forecast.', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ - forecastProgress: PROGRESS_STATES.ERROR, - }); - clearInterval(this.forecastChecker); + } + ), + MESSAGE_LEVEL.ERROR + ); + this.setState({ + forecastProgress: PROGRESS_STATES.ERROR, + }); + clearInterval(this.forecastChecker); + }); + }, FORECAST_STATS_POLL_FREQUENCY); + }; + + openModal = () => { + const job = this.props.job; + + if (typeof job === 'object') { + // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. + const { timefilter } = this.props.kibana.services.data.query.timefilter; + const bounds = timefilter.getActiveBounds(); + const statusFinishedQuery = { + term: { + forecast_status: FORECAST_REQUEST_STATE.FINISHED, + }, + }; + mlForecastService + .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) + .then(resp => { + this.setState({ + previousForecasts: resp.forecasts, }); - }, FORECAST_STATS_POLL_FREQUENCY); - }; - - openModal = () => { - const { intl } = this.props; - const job = this.props.job; - - if (typeof job === 'object') { - // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. - const bounds = timefilter.getActiveBounds(); - const statusFinishedQuery = { - term: { - forecast_status: FORECAST_REQUEST_STATE.FINISHED, - }, - }; - mlForecastService - .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) - .then(resp => { - this.setState({ - previousForecasts: resp.forecasts, + }) + .catch(resp => { + console.log('Time series forecast modal - error obtaining forecasts summary:', resp); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithObtainingListOfPreviousForecastsErrorMessage', + { + defaultMessage: 'Error obtaining list of previous forecasts', + } + ), + MESSAGE_LEVEL.ERROR + ); + }); + + // Display a warning about running a forecast if there is high number + // of partitioning fields. + const entityFieldNames = this.props.entities.map(entity => entity.fieldName); + if (entityFieldNames.length > 0) { + ml.getCardinalityOfFields({ + index: job.datafeed_config.indices, + fieldNames: entityFieldNames, + query: job.datafeed_config.query, + timeFieldName: job.data_description.time_field, + earliestMs: job.data_counts.earliest_record_timestamp, + latestMs: job.data_counts.latest_record_timestamp, + }) + .then(results => { + let numPartitions = 1; + Object.values(results).forEach(cardinality => { + numPartitions = numPartitions * cardinality; }); + if (numPartitions > WARN_NUM_PARTITIONS) { + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.dataContainsMorePartitionsMessage', + { + defaultMessage: + 'Note that this data contains more than {warnNumPartitions} ' + + 'partitions so running a forecast may take a long time and consume a high amount of resource', + values: { warnNumPartitions: WARN_NUM_PARTITIONS }, + } + ), + MESSAGE_LEVEL.WARNING + ); + } }) .catch(resp => { - console.log('Time series forecast modal - error obtaining forecasts summary:', resp); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithObtainingListOfPreviousForecastsErrorMessage', - defaultMessage: 'Error obtaining list of previous forecasts', - }), - MESSAGE_LEVEL.ERROR + console.log( + 'Time series forecast modal - error obtaining cardinality of fields:', + resp ); }); + } - // Display a warning about running a forecast if there is high number - // of partitioning fields. - const entityFieldNames = this.props.entities.map(entity => entity.fieldName); - if (entityFieldNames.length > 0) { - ml.getCardinalityOfFields({ - index: job.datafeed_config.indices, - fieldNames: entityFieldNames, - query: job.datafeed_config.query, - timeFieldName: job.data_description.time_field, - earliestMs: job.data_counts.earliest_record_timestamp, - latestMs: job.data_counts.latest_record_timestamp, - }) - .then(results => { - let numPartitions = 1; - Object.values(results).forEach(cardinality => { - numPartitions = numPartitions * cardinality; - }); - if (numPartitions > WARN_NUM_PARTITIONS) { - this.addMessage( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.dataContainsMorePartitionsMessage', - defaultMessage: - 'Note that this data contains more than {warnNumPartitions} ' + - 'partitions so running a forecast may take a long time and consume a high amount of resource', - }, - { warnNumPartitions: WARN_NUM_PARTITIONS } - ), - MESSAGE_LEVEL.WARNING - ); - } - }) - .catch(resp => { - console.log( - 'Time series forecast modal - error obtaining cardinality of fields:', - resp - ); - }); - } + this.setState({ isModalVisible: true }); + } + }; - this.setState({ isModalVisible: true }); - } - }; - - closeAfterRunningForecast = () => { - // Only close the dialog automatically after a forecast has run - // if the message bar is clear. Otherwise the user may not catch - // any messages returned in the forecast request stats. - if (this.state.messages.length === 0) { - // Wrap the close in a timeout to give the user a chance to see progress update. - setTimeout(() => { - this.closeModal(); - }, 1000); - } - }; + closeAfterRunningForecast = () => { + // Only close the dialog automatically after a forecast has run + // if the message bar is clear. Otherwise the user may not catch + // any messages returned in the forecast request stats. + if (this.state.messages.length === 0) { + // Wrap the close in a timeout to give the user a chance to see progress update. + setTimeout(() => { + this.closeModal(); + }, 1000); + } + }; - closeModal = () => { - if (this.forecastChecker !== null) { - clearInterval(this.forecastChecker); - } - this.setState(getDefaultState()); - }; - - render() { - // Forecasting disabled if detector has an over field or job created < 6.1.0. - let isForecastingDisabled = false; - let forecastingDisabledMessage = null; - const { intl, job } = this.props; - if (job !== undefined) { - const detector = job.analysis_config.detectors[this.props.detectorIndex]; - const overFieldName = detector.over_field_name; - if (overFieldName !== undefined) { - isForecastingDisabled = true; - forecastingDisabledMessage = intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingNotAvailableForPopulationDetectorsMessage', + closeModal = () => { + if (this.forecastChecker !== null) { + clearInterval(this.forecastChecker); + } + this.setState(getDefaultState()); + }; + + render() { + // Forecasting disabled if detector has an over field or job created < 6.1.0. + let isForecastingDisabled = false; + let forecastingDisabledMessage = null; + const { job } = this.props; + if (job !== undefined) { + const detector = job.analysis_config.detectors[this.props.detectorIndex]; + const overFieldName = detector.over_field_name; + if (overFieldName !== undefined) { + isForecastingDisabled = true; + forecastingDisabledMessage = i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingNotAvailableForPopulationDetectorsMessage', + { defaultMessage: 'Forecasting is not available for population detectors with an over field', - }); - } else if (isJobVersionGte(job, FORECAST_JOB_MIN_VERSION) === false) { - isForecastingDisabled = true; - forecastingDisabledMessage = intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingOnlyAvailableForJobsCreatedInSpecifiedVersionMessage', - defaultMessage: - 'Forecasting is only available for jobs created in version {minVersion} or later', - }, - { minVersion: FORECAST_JOB_MIN_VERSION } - ); - } + } + ); + } else if (isJobVersionGte(job, FORECAST_JOB_MIN_VERSION) === false) { + isForecastingDisabled = true; + forecastingDisabledMessage = i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingOnlyAvailableForJobsCreatedInSpecifiedVersionMessage', + { + defaultMessage: + 'Forecasting is only available for jobs created in version {minVersion} or later', + values: { minVersion: FORECAST_JOB_MIN_VERSION }, + } + ); } + } - const forecastButton = ( - - + + + ); + + return ( +
+ {isForecastingDisabled ? ( + + {forecastButton} + + ) : ( + forecastButton + )} + + {this.state.isModalVisible && ( + - - ); - - return ( -
- {isForecastingDisabled ? ( - - {forecastButton} - - ) : ( - forecastButton - )} - - {this.state.isModalVisible && ( - - )} -
- ); - } + )} +
+ ); } -); +} + +export const ForecastingModal = withKibana(ForecastingModalUI); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 2eaa4a907af66..3c639239757db 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -10,14 +10,12 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component } from 'react'; import useObservable from 'react-use/lib/useObservable'; import _ from 'lodash'; import d3 from 'd3'; import moment from 'moment'; -import chrome from 'ui/chrome'; - import { getSeverityWithLow, getMultiBucketImpactLabel, @@ -52,7 +50,7 @@ import { unhighlightFocusChartAnnotation, } from './timeseries_chart_annotations'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -62,7 +60,6 @@ const contextChartLineTopMargin = 3; const chartSpacing = 25; const swimlaneHeight = 30; const margin = { top: 10, right: 10, bottom: 15, left: 40 }; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); const ZOOM_INTERVAL_OPTIONS = [ { duration: moment.duration(1, 'h'), label: '1h' }, @@ -91,678 +88,765 @@ function getSvgHeight() { ); } -const TimeseriesChartIntl = injectI18n( - class TimeseriesChart extends React.Component { - static propTypes = { - annotation: PropTypes.object, - autoZoomDuration: PropTypes.number, - bounds: PropTypes.object, - contextAggregationInterval: PropTypes.object, - contextChartData: PropTypes.array, - contextForecastData: PropTypes.array, - contextChartSelected: PropTypes.func.isRequired, - detectorIndex: PropTypes.number, - focusAggregationInterval: PropTypes.object, - focusAnnotationData: PropTypes.array, - focusChartData: PropTypes.array, - focusForecastData: PropTypes.array, - modelPlotEnabled: PropTypes.bool.isRequired, - renderFocusChartOnly: PropTypes.bool.isRequired, - selectedJob: PropTypes.object, - showForecast: PropTypes.bool.isRequired, - showModelBounds: PropTypes.bool.isRequired, - svgWidth: PropTypes.number.isRequired, - swimlaneData: PropTypes.array, - zoomFrom: PropTypes.object, - zoomTo: PropTypes.object, - zoomFromFocusLoaded: PropTypes.object, - zoomToFocusLoaded: PropTypes.object, - }; - - rowMouseenterSubscriber = null; - rowMouseleaveSubscriber = null; - - componentWillUnmount() { - const element = d3.select(this.rootNode); - element.html(''); - - if (this.rowMouseenterSubscriber !== null) { - this.rowMouseenterSubscriber.unsubscribe(); - } - if (this.rowMouseleaveSubscriber !== null) { - this.rowMouseleaveSubscriber.unsubscribe(); - } +class TimeseriesChartIntl extends Component { + static propTypes = { + annotation: PropTypes.object, + autoZoomDuration: PropTypes.number, + bounds: PropTypes.object, + contextAggregationInterval: PropTypes.object, + contextChartData: PropTypes.array, + contextForecastData: PropTypes.array, + contextChartSelected: PropTypes.func.isRequired, + detectorIndex: PropTypes.number, + focusAggregationInterval: PropTypes.object, + focusAnnotationData: PropTypes.array, + focusChartData: PropTypes.array, + focusForecastData: PropTypes.array, + modelPlotEnabled: PropTypes.bool.isRequired, + renderFocusChartOnly: PropTypes.bool.isRequired, + selectedJob: PropTypes.object, + showForecast: PropTypes.bool.isRequired, + showModelBounds: PropTypes.bool.isRequired, + svgWidth: PropTypes.number.isRequired, + swimlaneData: PropTypes.array, + zoomFrom: PropTypes.object, + zoomTo: PropTypes.object, + zoomFromFocusLoaded: PropTypes.object, + zoomToFocusLoaded: PropTypes.object, + }; + + rowMouseenterSubscriber = null; + rowMouseleaveSubscriber = null; + + componentWillUnmount() { + const element = d3.select(this.rootNode); + element.html(''); + + if (this.rowMouseenterSubscriber !== null) { + this.rowMouseenterSubscriber.unsubscribe(); } + if (this.rowMouseleaveSubscriber !== null) { + this.rowMouseleaveSubscriber.unsubscribe(); + } + } - componentDidMount() { - const { svgWidth } = this.props; - - this.vizWidth = svgWidth - margin.left - margin.right; - const vizWidth = this.vizWidth; - - this.focusXScale = d3.time.scale().range([0, vizWidth]); - this.focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); - const focusXScale = this.focusXScale; - const focusYScale = this.focusYScale; - - this.focusXAxis = d3.svg - .axis() - .scale(focusXScale) - .orient('bottom') - .innerTickSize(-focusChartHeight) - .outerTickSize(0) - .tickPadding(10); - this.focusYAxis = d3.svg - .axis() - .scale(focusYScale) - .orient('left') - .innerTickSize(-vizWidth) - .outerTickSize(0) - .tickPadding(10); - - this.focusValuesLine = d3.svg - .line() - .x(function(d) { - return focusXScale(d.date); - }) - .y(function(d) { - return focusYScale(d.value); - }) - .defined(d => d.value !== null); - this.focusBoundedArea = d3.svg - .area() - .x(function(d) { - return focusXScale(d.date) || 1; - }) - .y0(function(d) { - return focusYScale(d.upper); - }) - .y1(function(d) { - return focusYScale(d.lower); - }) - .defined(d => d.lower !== null && d.upper !== null); - - this.contextXScale = d3.time.scale().range([0, vizWidth]); - this.contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); - - this.fieldFormat = undefined; - - // Annotations Brush - if (mlAnnotationsEnabled) { - this.annotateBrush = getAnnotationBrush.call(this); + componentDidMount() { + const { svgWidth } = this.props; + + this.vizWidth = svgWidth - margin.left - margin.right; + const vizWidth = this.vizWidth; + + this.focusXScale = d3.time.scale().range([0, vizWidth]); + this.focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + + this.focusXAxis = d3.svg + .axis() + .scale(focusXScale) + .orient('bottom') + .innerTickSize(-focusChartHeight) + .outerTickSize(0) + .tickPadding(10); + this.focusYAxis = d3.svg + .axis() + .scale(focusYScale) + .orient('left') + .innerTickSize(-vizWidth) + .outerTickSize(0) + .tickPadding(10); + + this.focusValuesLine = d3.svg + .line() + .x(function(d) { + return focusXScale(d.date); + }) + .y(function(d) { + return focusYScale(d.value); + }) + .defined(d => d.value !== null); + this.focusBoundedArea = d3.svg + .area() + .x(function(d) { + return focusXScale(d.date) || 1; + }) + .y0(function(d) { + return focusYScale(d.upper); + }) + .y1(function(d) { + return focusYScale(d.lower); + }) + .defined(d => d.lower !== null && d.upper !== null); + + this.contextXScale = d3.time.scale().range([0, vizWidth]); + this.contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); + + this.fieldFormat = undefined; + + // Annotations Brush + this.annotateBrush = getAnnotationBrush.call(this); + + // brush for focus brushing + this.brush = d3.svg.brush(); + + this.mask = undefined; + + // Listeners for mouseenter/leave events for rows in the table + // to highlight the corresponding anomaly mark in the focus chart. + const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this); + const boundHighlightFocusChartAnnotation = highlightFocusChartAnnotation.bind(this); + function tableRecordMousenterListener({ record, type = 'anomaly' }) { + if (type === 'anomaly') { + highlightFocusChartAnomaly(record); + } else if (type === 'annotation') { + boundHighlightFocusChartAnnotation(record); } + } - // brush for focus brushing - this.brush = d3.svg.brush(); - - this.mask = undefined; - - // Listeners for mouseenter/leave events for rows in the table - // to highlight the corresponding anomaly mark in the focus chart. - const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this); - const boundHighlightFocusChartAnnotation = highlightFocusChartAnnotation.bind(this); - function tableRecordMousenterListener({ record, type = 'anomaly' }) { - if (type === 'anomaly') { - highlightFocusChartAnomaly(record); - } else if (type === 'annotation') { - boundHighlightFocusChartAnnotation(record); - } + const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this); + const boundUnhighlightFocusChartAnnotation = unhighlightFocusChartAnnotation.bind(this); + function tableRecordMouseleaveListener({ record, type = 'anomaly' }) { + if (type === 'anomaly') { + unhighlightFocusChartAnomaly(record); + } else { + boundUnhighlightFocusChartAnnotation(record); } + } - const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this); - const boundUnhighlightFocusChartAnnotation = unhighlightFocusChartAnnotation.bind(this); - function tableRecordMouseleaveListener({ record, type = 'anomaly' }) { - if (type === 'anomaly') { - unhighlightFocusChartAnomaly(record); - } else { - boundUnhighlightFocusChartAnnotation(record); - } - } + this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe( + tableRecordMousenterListener + ); + this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe( + tableRecordMouseleaveListener + ); - this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe( - tableRecordMousenterListener - ); - this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe( - tableRecordMouseleaveListener - ); + this.renderChart(); + this.drawContextChartSelection(); + this.renderFocusChart(); + } + componentDidUpdate() { + if (this.props.renderFocusChartOnly === false) { this.renderChart(); this.drawContextChartSelection(); - this.renderFocusChart(); } - componentDidUpdate() { - if (this.props.renderFocusChartOnly === false) { - this.renderChart(); - this.drawContextChartSelection(); - } - - this.renderFocusChart(); - - if (mlAnnotationsEnabled && this.props.annotation === null) { - const chartElement = d3.select(this.rootNode); - chartElement.select('g.mlAnnotationBrush').call(this.annotateBrush.extent([0, 0])); - } - } + this.renderFocusChart(); - renderChart() { - const { - contextChartData, - contextForecastData, - detectorIndex, - modelPlotEnabled, - selectedJob, - svgWidth, - } = this.props; - - const createFocusChart = this.createFocusChart.bind(this); - const drawContextElements = this.drawContextElements.bind(this); - const focusXScale = this.focusXScale; - const focusYAxis = this.focusYAxis; - const focusYScale = this.focusYScale; - - const svgHeight = getSvgHeight(); - - // Clear any existing elements from the visualization, - // then build the svg elements for the bubble chart. + if (this.props.annotation === null) { const chartElement = d3.select(this.rootNode); - chartElement.selectAll('*').remove(); - - if (typeof selectedJob !== 'undefined') { - this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); - } else { - return; - } + chartElement.select('g.mlAnnotationBrush').call(this.annotateBrush.extent([0, 0])); + } + } - if (contextChartData === undefined) { - return; - } + renderChart() { + const { + contextChartData, + contextForecastData, + detectorIndex, + modelPlotEnabled, + selectedJob, + svgWidth, + } = this.props; + + const createFocusChart = this.createFocusChart.bind(this); + const drawContextElements = this.drawContextElements.bind(this); + const focusXScale = this.focusXScale; + const focusYAxis = this.focusYAxis; + const focusYScale = this.focusYScale; + + const svgHeight = getSvgHeight(); + + // Clear any existing elements from the visualization, + // then build the svg elements for the bubble chart. + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('*').remove(); + + if (typeof selectedJob !== 'undefined') { + this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); + } else { + return; + } - const fieldFormat = this.fieldFormat; + if (contextChartData === undefined) { + return; + } - const svg = chartElement - .append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); + const fieldFormat = this.fieldFormat; - let contextDataMin; - let contextDataMax; - if ( - modelPlotEnabled === true || - (contextForecastData !== undefined && contextForecastData.length > 0) - ) { - const combinedData = - contextForecastData === undefined - ? contextChartData - : contextChartData.concat(contextForecastData); + const svg = chartElement + .append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); - contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); - contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); - } else { - contextDataMin = d3.min(contextChartData, d => d.value); - contextDataMax = d3.max(contextChartData, d => d.value); - } + let contextDataMin; + let contextDataMax; + if ( + modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0) + ) { + const combinedData = + contextForecastData === undefined + ? contextChartData + : contextChartData.concat(contextForecastData); + + contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); + contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); + } else { + contextDataMin = d3.min(contextChartData, d => d.value); + contextDataMax = d3.max(contextChartData, d => d.value); + } - // Set the size of the left margin according to the width of the largest y axis tick label. - // The min / max of the aggregated context chart data may be less than the min / max of the - // data which is displayed in the focus chart which is likely to be plotted at a lower - // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow - // for extra space for chart labels which may have higher values than the context data - // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. - const ceiledMax = - contextDataMax > 0 - ? Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) - : contextDataMax; - - const flooredMin = - contextDataMin >= 0 - ? contextDataMin - : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); - - // Temporarily set the domain of the focus y axis to the min / max of the full context chart - // data range so that we can measure the maximum tick label width on temporary text elements. - focusYScale.domain([flooredMin, ceiledMax]); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - tempLabelText - .selectAll('text.temp.axis') - .data(focusYScale.ticks()) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - return focusYScale.tickFormat()(d); - } - }) - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + focusYAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - margin.left = Math.max(maxYAxisLabelWidth, 40); - this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); - focusXScale.range([0, this.vizWidth]); - focusYAxis.innerTickSize(-this.vizWidth); - - const focus = svg - .append('g') - .attr('class', 'focus-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - const context = svg - .append('g') - .attr('class', 'context-chart') - .attr( - 'transform', - 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')' + // Set the size of the left margin according to the width of the largest y axis tick label. + // The min / max of the aggregated context chart data may be less than the min / max of the + // data which is displayed in the focus chart which is likely to be plotted at a lower + // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow + // for extra space for chart labels which may have higher values than the context data + // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. + const ceiledMax = + contextDataMax > 0 + ? Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) + : contextDataMax; + + const flooredMin = + contextDataMin >= 0 + ? contextDataMin + : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); + + // Temporarily set the domain of the focus y axis to the min / max of the full context chart + // data range so that we can measure the maximum tick label width on temporary text elements. + focusYScale.domain([flooredMin, ceiledMax]); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + tempLabelText + .selectAll('text.temp.axis') + .data(focusYScale.ticks()) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return focusYScale.tickFormat()(d); + } + }) + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + focusYAxis.tickPadding(), + maxYAxisLabelWidth ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = Math.max(maxYAxisLabelWidth, 40); + this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); + focusXScale.range([0, this.vizWidth]); + focusYAxis.innerTickSize(-this.vizWidth); + + const focus = svg + .append('g') + .attr('class', 'focus-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + const context = svg + .append('g') + .attr('class', 'context-chart') + .attr( + 'transform', + 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')' + ); - // Mask to hide annotations overflow - if (mlAnnotationsEnabled) { - const annotationsMask = svg - .append('defs') - .append('mask') - .attr('id', ANNOTATION_MASK_ID); - - annotationsMask - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', this.vizWidth) - .attr('height', focusHeight) - .style('fill', 'white'); - } + // Mask to hide annotations overflow + const annotationsMask = svg + .append('defs') + .append('mask') + .attr('id', ANNOTATION_MASK_ID); + + annotationsMask + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', this.vizWidth) + .attr('height', focusHeight) + .style('fill', 'white'); + + // Draw each of the component elements. + createFocusChart(focus, this.vizWidth, focusHeight); + drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + } - // Draw each of the component elements. - createFocusChart(focus, this.vizWidth, focusHeight); - drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + contextChartInitialized = false; + drawContextChartSelection() { + const { + contextChartData, + contextChartSelected, + contextForecastData, + zoomFrom, + zoomTo, + } = this.props; + + if (contextChartData === undefined) { + return; } - contextChartInitialized = false; - drawContextChartSelection() { - const { - contextChartData, - contextChartSelected, - contextForecastData, - zoomFrom, - zoomTo, - } = this.props; - - if (contextChartData === undefined) { - return; - } - - // Make appropriate selection in the context chart to trigger loading of the focus chart. - let focusLoadFrom; - let focusLoadTo; - const contextXMin = this.contextXScale.domain()[0].getTime(); - const contextXMax = this.contextXScale.domain()[1].getTime(); + // Make appropriate selection in the context chart to trigger loading of the focus chart. + let focusLoadFrom; + let focusLoadTo; + const contextXMin = this.contextXScale.domain()[0].getTime(); + const contextXMax = this.contextXScale.domain()[1].getTime(); - let combinedData = contextChartData; - if (contextForecastData !== undefined) { - combinedData = combinedData.concat(contextForecastData); - } - - if (zoomFrom) { - focusLoadFrom = zoomFrom.getTime(); - } else { - focusLoadFrom = _.reduce( - combinedData, - (memo, point) => Math.min(memo, point.date.getTime()), - new Date(2099, 12, 31).getTime() - ); - } - focusLoadFrom = Math.max(focusLoadFrom, contextXMin); + let combinedData = contextChartData; + if (contextForecastData !== undefined) { + combinedData = combinedData.concat(contextForecastData); + } - if (zoomTo) { - focusLoadTo = zoomTo.getTime(); - } else { - focusLoadTo = _.reduce( - combinedData, - (memo, point) => Math.max(memo, point.date.getTime()), - 0 - ); - } - focusLoadTo = Math.min(focusLoadTo, contextXMax); + if (zoomFrom) { + focusLoadFrom = zoomFrom.getTime(); + } else { + focusLoadFrom = _.reduce( + combinedData, + (memo, point) => Math.min(memo, point.date.getTime()), + new Date(2099, 12, 31).getTime() + ); + } + focusLoadFrom = Math.max(focusLoadFrom, contextXMin); + + if (zoomTo) { + focusLoadTo = zoomTo.getTime(); + } else { + focusLoadTo = _.reduce( + combinedData, + (memo, point) => Math.max(memo, point.date.getTime()), + 0 + ); + } + focusLoadTo = Math.min(focusLoadTo, contextXMax); - const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; - this.setBrushVisibility(brushVisibility); + const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; + this.setBrushVisibility(brushVisibility); - if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { - this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); - const newSelectedBounds = { - min: moment(new Date(focusLoadFrom)), - max: moment(focusLoadFrom), - }; + if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { + this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); + const newSelectedBounds = { + min: moment(new Date(focusLoadFrom)), + max: moment(focusLoadFrom), + }; + this.selectedBounds = newSelectedBounds; + } else { + const contextXScaleDomain = this.contextXScale.domain(); + const newSelectedBounds = { + min: moment(new Date(contextXScaleDomain[0])), + max: moment(contextXScaleDomain[1]), + }; + if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { this.selectedBounds = newSelectedBounds; - } else { - const contextXScaleDomain = this.contextXScale.domain(); - const newSelectedBounds = { - min: moment(new Date(contextXScaleDomain[0])), - max: moment(contextXScaleDomain[1]), - }; - if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { - this.selectedBounds = newSelectedBounds; - if (this.contextChartInitialized === false) { - this.contextChartInitialized = true; - contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); - } + if (this.contextChartInitialized === false) { + this.contextChartInitialized = true; + contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); } } } + } - createFocusChart(fcsGroup, fcsWidth, fcsHeight) { - // Split out creation of the focus chart from the rendering, - // as we want to re-render the paths and points when the zoom area changes. - - const { contextForecastData } = this.props; - - // Add a group at the top to display info on the chart aggregation interval - // and links to set the brush span to 1h, 1d, 1w etc. - const zoomGroup = fcsGroup.append('g').attr('class', 'focus-zoom'); - zoomGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', focusZoomPanelHeight) - .attr('class', 'chart-border'); - this.createZoomInfoElements(zoomGroup, fcsWidth); - - // Create the elements for annotations - if (mlAnnotationsEnabled) { - const annotateBrush = this.annotateBrush.bind(this); - - let brushX = 0; - let brushWidth = 0; - - if (this.props.annotation !== null) { - // If the annotation brush is showing, set it to the same position - brushX = this.focusXScale(this.props.annotation.timestamp); - brushWidth = getAnnotationWidth(this.props.annotation, this.focusXScale); - } + createFocusChart(fcsGroup, fcsWidth, fcsHeight) { + // Split out creation of the focus chart from the rendering, + // as we want to re-render the paths and points when the zoom area changes. + + const { contextForecastData } = this.props; + + // Add a group at the top to display info on the chart aggregation interval + // and links to set the brush span to 1h, 1d, 1w etc. + const zoomGroup = fcsGroup.append('g').attr('class', 'focus-zoom'); + zoomGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', focusZoomPanelHeight) + .attr('class', 'chart-border'); + this.createZoomInfoElements(zoomGroup, fcsWidth); + + // Create the elements for annotations + const annotateBrush = this.annotateBrush.bind(this); + + let brushX = 0; + let brushWidth = 0; + + if (this.props.annotation !== null) { + // If the annotation brush is showing, set it to the same position + brushX = this.focusXScale(this.props.annotation.timestamp); + brushWidth = getAnnotationWidth(this.props.annotation, this.focusXScale); + } - fcsGroup - .append('g') - .attr('class', 'mlAnnotationBrush') - .call(annotateBrush) - .selectAll('rect') - .attr('x', brushX) - .attr('y', focusZoomPanelHeight) - .attr('width', brushWidth) - .attr('height', focusChartHeight); - - fcsGroup.append('g').classed('mlAnnotations', true); - } + fcsGroup + .append('g') + .attr('class', 'mlAnnotationBrush') + .call(annotateBrush) + .selectAll('rect') + .attr('x', brushX) + .attr('y', focusZoomPanelHeight) + .attr('width', brushWidth) + .attr('height', focusChartHeight); + + fcsGroup.append('g').classed('mlAnnotations', true); + + // Add border round plot area. + fcsGroup + .append('rect') + .attr('x', 0) + .attr('y', focusZoomPanelHeight) + .attr('width', fcsWidth) + .attr('height', focusChartHeight) + .attr('class', 'chart-border'); + + // Add background for x axis. + const xAxisBg = fcsGroup.append('g').attr('class', 'x-axis-background'); + xAxisBg + .append('rect') + .attr('x', 0) + .attr('y', fcsHeight) + .attr('width', fcsWidth) + .attr('height', chartSpacing); + xAxisBg + .append('line') + .attr('x1', 0) + .attr('y1', fcsHeight) + .attr('x2', 0) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg + .append('line') + .attr('x1', fcsWidth) + .attr('y1', fcsHeight) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg + .append('line') + .attr('x1', 0) + .attr('y1', fcsHeight + chartSpacing) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + + const axes = fcsGroup.append('g'); + axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + fcsHeight + ')'); + axes.append('g').attr('class', 'y axis'); + + // Create the elements for the metric value line and model bounds area. + fcsGroup.append('path').attr('class', 'area bounds'); + fcsGroup.append('path').attr('class', 'values-line'); + fcsGroup.append('g').attr('class', 'focus-chart-markers'); + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData) { + fcsGroup.append('path').attr('class', 'area forecast'); + fcsGroup.append('path').attr('class', 'values-line forecast'); + fcsGroup.append('g').attr('class', 'focus-chart-markers forecast'); + } - // Add border round plot area. - fcsGroup - .append('rect') - .attr('x', 0) - .attr('y', focusZoomPanelHeight) - .attr('width', fcsWidth) - .attr('height', focusChartHeight) - .attr('class', 'chart-border'); - - // Add background for x axis. - const xAxisBg = fcsGroup.append('g').attr('class', 'x-axis-background'); - xAxisBg - .append('rect') - .attr('x', 0) - .attr('y', fcsHeight) - .attr('width', fcsWidth) - .attr('height', chartSpacing); - xAxisBg - .append('line') - .attr('x1', 0) - .attr('y1', fcsHeight) - .attr('x2', 0) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg - .append('line') - .attr('x1', fcsWidth) - .attr('y1', fcsHeight) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg - .append('line') - .attr('x1', 0) - .attr('y1', fcsHeight + chartSpacing) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - - const axes = fcsGroup.append('g'); - axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + fcsHeight + ')'); - axes.append('g').attr('class', 'y axis'); - - // Create the elements for the metric value line and model bounds area. - fcsGroup.append('path').attr('class', 'area bounds'); - fcsGroup.append('path').attr('class', 'values-line'); - fcsGroup.append('g').attr('class', 'focus-chart-markers'); - - // Create the path elements for the forecast value line and bounds area. - if (contextForecastData) { - fcsGroup.append('path').attr('class', 'area forecast'); - fcsGroup.append('path').attr('class', 'values-line forecast'); - fcsGroup.append('g').attr('class', 'focus-chart-markers forecast'); - } + fcsGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', fcsHeight + 24) + .attr('class', 'chart-border chart-border-highlight'); + } - fcsGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', fcsHeight + 24) - .attr('class', 'chart-border chart-border-highlight'); + renderFocusChart() { + const { + focusAggregationInterval, + focusAnnotationData, + focusChartData, + focusForecastData, + modelPlotEnabled, + selectedJob, + showAnnotations, + showForecast, + showModelBounds, + + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.props; + + if (focusChartData === undefined) { + return; } - renderFocusChart() { - const { - focusAggregationInterval, - focusAnnotationData, - focusChartData, - focusForecastData, - modelPlotEnabled, - selectedJob, - showAnnotations, - showForecast, - showModelBounds, - intl, - zoomFromFocusLoaded, - zoomToFocusLoaded, - } = this.props; - - if (focusChartData === undefined) { - return; - } + const data = focusChartData; - const data = focusChartData; + const contextYScale = this.contextYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); - const contextYScale = this.contextYScale; - const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + const focusChart = d3.select('.focus-chart'); - const focusChart = d3.select('.focus-chart'); + // Update the plot interval labels. + const focusAggInt = focusAggregationInterval.expression; + const bucketSpan = selectedJob.analysis_config.bucket_span; + const chartElement = d3.select(this.rootNode); + chartElement.select('.zoom-aggregation-interval').text( + i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel', { + defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})', + values: { focusAggInt, bucketSpan }, + }) + ); - // Update the plot interval labels. - const focusAggInt = focusAggregationInterval.expression; - const bucketSpan = selectedJob.analysis_config.bucket_span; - const chartElement = d3.select(this.rootNode); - chartElement.select('.zoom-aggregation-interval').text( - intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel', - defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})', - }, - { focusAggInt, bucketSpan } - ) - ); + // Render the axes. - // Render the axes. + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + if (zoomFromFocusLoaded === undefined || zoomToFocusLoaded === undefined) { + return; + } + const bounds = { + min: moment(zoomFromFocusLoaded.getTime()), + max: moment(zoomToFocusLoaded.getTime()), + }; - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - if (zoomFromFocusLoaded === undefined || zoomToFocusLoaded === undefined) { - return; + const aggMs = focusAggregationInterval.asMilliseconds(); + const earliest = moment(Math.floor(bounds.min.valueOf() / aggMs) * aggMs); + const latest = moment(Math.ceil(bounds.max.valueOf() / aggMs) * aggMs); + this.focusXScale.domain([earliest.toDate(), latest.toDate()]); + + // Calculate the y-axis domain. + if ( + focusChartData.length > 0 || + (focusForecastData !== undefined && focusForecastData.length > 0) + ) { + if (this.fieldFormat !== undefined) { + this.focusYAxis.tickFormat(d => this.fieldFormat.convert(d, 'text')); + } else { + // Use default tick formatter. + this.focusYAxis.tickFormat(null); } - const bounds = { - min: moment(zoomFromFocusLoaded.getTime()), - max: moment(zoomToFocusLoaded.getTime()), - }; - const aggMs = focusAggregationInterval.asMilliseconds(); - const earliest = moment(Math.floor(bounds.min.valueOf() / aggMs) * aggMs); - const latest = moment(Math.ceil(bounds.max.valueOf() / aggMs) * aggMs); - this.focusXScale.domain([earliest.toDate(), latest.toDate()]); + // Calculate the min/max of the metric data and the forecast data. + let yMin = 0; + let yMax = 0; - // Calculate the y-axis domain. - if ( - focusChartData.length > 0 || - (focusForecastData !== undefined && focusForecastData.length > 0) - ) { - if (this.fieldFormat !== undefined) { - this.focusYAxis.tickFormat(d => this.fieldFormat.convert(d, 'text')); - } else { - // Use default tick formatter. - this.focusYAxis.tickFormat(null); - } - - // Calculate the min/max of the metric data and the forecast data. - let yMin = 0; - let yMax = 0; + let combinedData = data; + if (focusForecastData !== undefined && focusForecastData.length > 0) { + combinedData = data.concat(focusForecastData); + } - let combinedData = data; - if (focusForecastData !== undefined && focusForecastData.length > 0) { - combinedData = data.concat(focusForecastData); + yMin = d3.min(combinedData, 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; } - - yMin = d3.min(combinedData, 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; - } - if (d.lower !== undefined) { - if (metricValue !== null && metricValue !== undefined) { - return Math.min(metricValue, d.lower); - } else { - // Set according to the minimum of the lower of the model plot results. - return d.lower; - } - } - return metricValue; - }); - yMax = d3.max(combinedData, 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 d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue; - }); - - if (yMax === yMin) { - if ( - this.contextYScale.domain()[0] !== contextYScale.domain()[1] && - yMin >= contextYScale.domain()[0] && - yMax <= contextYScale.domain()[1] - ) { - // Set the focus chart limits to be the same as the context chart. - yMin = contextYScale.domain()[0]; - yMax = contextYScale.domain()[1]; + if (d.lower !== undefined) { + if (metricValue !== null && metricValue !== undefined) { + return Math.min(metricValue, d.lower); } else { - yMin -= yMin * 0.05; - yMax += yMax * 0.05; + // Set according to the minimum of the lower of the model plot results. + return d.lower; } } + return metricValue; + }); + yMax = d3.max(combinedData, 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 d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue; + }); - // if annotations are present, we extend yMax to avoid overlap - // between annotation labels, chart lines and anomalies. - if (mlAnnotationsEnabled && focusAnnotationData && focusAnnotationData.length > 0) { - const levels = getAnnotationLevels(focusAnnotationData); - const maxLevel = d3.max(Object.keys(levels).map(key => levels[key])); - // TODO needs revisiting to be a more robust normalization - yMax = yMax * (1 + (maxLevel + 1) / 5); + if (yMax === yMin) { + if ( + this.contextYScale.domain()[0] !== contextYScale.domain()[1] && + yMin >= contextYScale.domain()[0] && + yMax <= contextYScale.domain()[1] + ) { + // Set the focus chart limits to be the same as the context chart. + yMin = contextYScale.domain()[0]; + yMax = contextYScale.domain()[1]; + } else { + yMin -= yMin * 0.05; + yMax += yMax * 0.05; } - this.focusYScale.domain([yMin, yMax]); - } else { - // Display 10 unlabelled ticks. - this.focusYScale.domain([0, 10]); - this.focusYAxis.tickFormat(''); } - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - focusChart.select('.x.axis').call( - this.focusXAxis - .ticks(numTicksForDateFormat(this.vizWidth), xAxisTickFormat) - .tickFormat(d => { - return moment(d).format(xAxisTickFormat); - }) - ); - focusChart.select('.y.axis').call(this.focusYAxis); - - filterAxisLabels(focusChart.select('.x.axis'), this.vizWidth); - - // Render the bounds area and values line. - if (modelPlotEnabled === true) { - focusChart - .select('.area.bounds') - .attr('d', this.focusBoundedArea(data)) - .classed('hidden', !showModelBounds); + // if annotations are present, we extend yMax to avoid overlap + // between annotation labels, chart lines and anomalies. + if (focusAnnotationData && focusAnnotationData.length > 0) { + const levels = getAnnotationLevels(focusAnnotationData); + const maxLevel = d3.max(Object.keys(levels).map(key => levels[key])); + // TODO needs revisiting to be a more robust normalization + yMax = yMax * (1 + (maxLevel + 1) / 5); } + this.focusYScale.domain([yMin, yMax]); + } else { + // Display 10 unlabelled ticks. + this.focusYScale.domain([0, 10]); + this.focusYAxis.tickFormat(''); + } - if (mlAnnotationsEnabled) { - renderAnnotations( - focusChart, - focusAnnotationData, - focusZoomPanelHeight, - focusChartHeight, - this.focusXScale, - showAnnotations, - showFocusChartTooltip - ); + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + focusChart.select('.x.axis').call( + this.focusXAxis.ticks(numTicksForDateFormat(this.vizWidth), xAxisTickFormat).tickFormat(d => { + return moment(d).format(xAxisTickFormat); + }) + ); + focusChart.select('.y.axis').call(this.focusYAxis); + + filterAxisLabels(focusChart.select('.x.axis'), this.vizWidth); + + // Render the bounds area and values line. + if (modelPlotEnabled === true) { + focusChart + .select('.area.bounds') + .attr('d', this.focusBoundedArea(data)) + .classed('hidden', !showModelBounds); + } - // disable brushing (creation of annotations) when annotations aren't shown - focusChart.select('.mlAnnotationBrush').style('display', showAnnotations ? null : 'none'); - } + renderAnnotations( + focusChart, + focusAnnotationData, + focusZoomPanelHeight, + focusChartHeight, + this.focusXScale, + showAnnotations, + showFocusChartTooltip + ); + + // disable brushing (creation of annotations) when annotations aren't shown + focusChart.select('.mlAnnotationBrush').style('display', showAnnotations ? null : 'none'); + + focusChart.select('.values-line').attr('d', this.focusValuesLine(data)); + drawLineChartDots(data, focusChart, this.focusValuesLine); + + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps, with no anomalies) + // or for multi-bucket anomalies. + const dots = d3 + .select('.focus-chart-markers') + .selectAll('.metric-value') + .data( + data.filter( + d => + (d.value !== null || typeof d.anomalyScore === 'number') && + !showMultiBucketAnomalyMarker(d) + ) + ); - focusChart.select('.values-line').attr('d', this.focusValuesLine(data)); - drawLineChartDots(data, focusChart, this.focusValuesLine); + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('mouseover', function(d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + dots + .attr('cx', d => { + return this.focusXScale(d.date); + }) + .attr('cy', d => { + return this.focusYScale(d.value); + }) + .attr('class', d => { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore')) { + markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; + } + return markerClass; + }); - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps, with no anomalies) - // or for multi-bucket anomalies. - const dots = d3 - .select('.focus-chart-markers') + // Render cross symbols for any multi-bucket anomalies. + const multiBucketMarkers = d3 + .select('.focus-chart-markers') + .selectAll('.multi-bucket') + .data(data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)); + + // Remove multi-bucket markers that are no longer needed. + multiBucketMarkers.exit().remove(); + + // Add any new markers that are needed i.e. if number of multi-bucket points has increased. + multiBucketMarkers + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .on('mouseover', function(d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all markers to new positions. + multiBucketMarkers + .attr( + 'transform', + d => `translate(${this.focusXScale(d.date)}, ${this.focusYScale(d.value)})` + ) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`); + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = d3 + .select('.focus-chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => this.focusYScale(d.value) - 3); + + // Plot any forecast data in scope. + if (focusForecastData !== undefined) { + focusChart + .select('.area.forecast') + .attr('d', this.focusBoundedArea(focusForecastData)) + .classed('hidden', !showForecast); + focusChart + .select('.values-line.forecast') + .attr('d', this.focusValuesLine(focusForecastData)) + .classed('hidden', !showForecast); + + const forecastDots = d3 + .select('.focus-chart-markers.forecast') .selectAll('.metric-value') - .data( - data.filter( - d => - (d.value !== null || typeof d.anomalyScore === 'number') && - !showMultiBucketAnomalyMarker(d) - ) - ); + .data(focusForecastData); - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots + // Remove dots that are no longer needed i.e. if number of forecast points has decreased. + forecastDots.exit().remove(); + // Create any new dots that are needed i.e. if number of forecast points has increased. + forecastDots .enter() .append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) @@ -772,755 +856,603 @@ const TimeseriesChartIntl = injectI18n( .on('mouseout', () => mlChartTooltipService.hide()); // Update all dots to new positions. - dots + forecastDots .attr('cx', d => { return this.focusXScale(d.date); }) .attr('cy', d => { return this.focusYScale(d.value); }) - .attr('class', d => { - let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore')) { - markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; - } - return markerClass; - }); - - // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = d3 - .select('.focus-chart-markers') - .selectAll('.multi-bucket') - .data( - data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true) - ); - - // Remove multi-bucket markers that are no longer needed. - multiBucketMarkers.exit().remove(); + .attr('class', 'metric-value') + .classed('hidden', !showForecast); + } + } - // Add any new markers that are needed i.e. if number of multi-bucket points has increased. - multiBucketMarkers - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .on('mouseover', function(d) { - showFocusChartTooltip(d, this); + createZoomInfoElements(zoomGroup, fcsWidth) { + const { autoZoomDuration, bounds, modelPlotEnabled } = this.props; + + const setZoomInterval = this.setZoomInterval.bind(this); + + // Create zoom duration links applicable for the current time span. + // Don't add links for any durations which would give a brush extent less than 10px. + const boundsSecs = bounds.max.unix() - bounds.min.unix(); + const minSecs = (10 / this.vizWidth) * boundsSecs; + + let xPos = 10; + const zoomLabel = zoomGroup + .append('text') + .attr('x', xPos) + .attr('y', 17) + .attr('class', 'zoom-info-text') + .text( + i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel', { + defaultMessage: 'Zoom:', }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all markers to new positions. - multiBucketMarkers - .attr( - 'transform', - d => `translate(${this.focusXScale(d.date)}, ${this.focusYScale(d.value)})` - ) - .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`); - - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = d3 - .select('.focus-chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); + ); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr('y', d => this.focusYScale(d.value) - 3); - - // Plot any forecast data in scope. - if (focusForecastData !== undefined) { - focusChart - .select('.area.forecast') - .attr('d', this.focusBoundedArea(focusForecastData)) - .classed('hidden', !showForecast); - focusChart - .select('.values-line.forecast') - .attr('d', this.focusValuesLine(focusForecastData)) - .classed('hidden', !showForecast); - - const forecastDots = d3 - .select('.focus-chart-markers.forecast') - .selectAll('.metric-value') - .data(focusForecastData); - - // Remove dots that are no longer needed i.e. if number of forecast points has decreased. - forecastDots.exit().remove(); - // Create any new dots that are needed i.e. if number of forecast points has increased. - forecastDots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .on('mouseover', function(d) { - showFocusChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - forecastDots - .attr('cx', d => { - return this.focusXScale(d.date); - }) - .attr('cy', d => { - return this.focusYScale(d.value); - }) - .attr('class', 'metric-value') - .classed('hidden', !showForecast); + const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }]; + _.each(ZOOM_INTERVAL_OPTIONS, option => { + if (option.duration.asSeconds() > minSecs && option.duration.asSeconds() < boundsSecs) { + zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); } - } - - createZoomInfoElements(zoomGroup, fcsWidth) { - const { autoZoomDuration, bounds, modelPlotEnabled, intl } = this.props; - - const setZoomInterval = this.setZoomInterval.bind(this); - - // Create zoom duration links applicable for the current time span. - // Don't add links for any durations which would give a brush extent less than 10px. - const boundsSecs = bounds.max.unix() - bounds.min.unix(); - const minSecs = (10 / this.vizWidth) * boundsSecs; - - let xPos = 10; - const zoomLabel = zoomGroup + }); + xPos += zoomLabel.node().getBBox().width + 4; + + _.each(zoomOptions, option => { + const text = zoomGroup + .append('a') + .attr('data-ms', option.durationMs) + .attr('href', '') .append('text') .attr('x', xPos) .attr('y', 17) .attr('class', 'zoom-info-text') - .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel', - defaultMessage: 'Zoom:', - }) - ); - - const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }]; - _.each(ZOOM_INTERVAL_OPTIONS, option => { - if (option.duration.asSeconds() > minSecs && option.duration.asSeconds() < boundsSecs) { - zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); - } - }); - xPos += zoomLabel.node().getBBox().width + 4; - - _.each(zoomOptions, option => { - const text = zoomGroup - .append('a') - .attr('data-ms', option.durationMs) - .attr('href', '') - .append('text') - .attr('x', xPos) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text(option.label); - - xPos += text.node().getBBox().width + 4; - }); + .text(option.label); + + xPos += text.node().getBBox().width + 4; + }); + + zoomGroup + .append('text') + .attr('x', xPos + 6) + .attr('y', 17) + .attr('class', 'zoom-info-text zoom-aggregation-interval') + .text( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomGroupAggregationIntervalLabel', + { + defaultMessage: '(aggregation interval: , bucket span: )', + } + ) + ); - zoomGroup + if (modelPlotEnabled === false) { + const modelPlotLabel = zoomGroup .append('text') - .attr('x', xPos + 6) + .attr('x', 300) .attr('y', 17) - .attr('class', 'zoom-info-text zoom-aggregation-interval') + .attr('class', 'zoom-info-text') .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomGroupAggregationIntervalLabel', - defaultMessage: '(aggregation interval: , bucket span: )', - }) - ); - - if (modelPlotEnabled === false) { - const modelPlotLabel = zoomGroup - .append('text') - .attr('x', 300) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelBoundsNotAvailableLabel', + i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelBoundsNotAvailableLabel', + { defaultMessage: 'Model bounds are not available', - }) - ); - - modelPlotLabel.attr('x', fcsWidth - (modelPlotLabel.node().getBBox().width + 10)); - } + } + ) + ); - const chartElement = d3.select(this.rootNode); - chartElement.selectAll('.focus-zoom a').on('click', function() { - d3.event.preventDefault(); - setZoomInterval(d3.select(this).attr('data-ms')); - }); + modelPlotLabel.attr('x', fcsWidth - (modelPlotLabel.node().getBBox().width + 10)); } - drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { - const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; - - const data = contextChartData; - - this.contextXScale = d3.time - .scale() - .range([0, cxtWidth]) - .domain(this.calculateContextXAxisDomain()); + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('.focus-zoom a').on('click', function() { + d3.event.preventDefault(); + setZoomInterval(d3.select(this).attr('data-ms')); + }); + } - const combinedData = - contextForecastData === undefined ? data : data.concat(contextForecastData); - const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { + const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; + + const data = contextChartData; + + this.contextXScale = d3.time + .scale() + .range([0, cxtWidth]) + .domain(this.calculateContextXAxisDomain()); + + const combinedData = + contextForecastData === undefined ? data : data.concat(contextForecastData); + const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + _.each(combinedData, item => { + valuesRange.min = Math.min(item.value, valuesRange.min); + valuesRange.max = Math.max(item.value, valuesRange.max); + }); + let dataMin = valuesRange.min; + let dataMax = valuesRange.max; + const chartLimits = { min: dataMin, max: dataMax }; + + if ( + modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0) + ) { + const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, item => { - valuesRange.min = Math.min(item.value, valuesRange.min); - valuesRange.max = Math.max(item.value, valuesRange.max); + boundsRange.min = Math.min(item.lower, boundsRange.min); + boundsRange.max = Math.max(item.upper, boundsRange.max); }); - let dataMin = valuesRange.min; - let dataMax = valuesRange.max; - const chartLimits = { min: dataMin, max: dataMax }; + dataMin = Math.min(dataMin, boundsRange.min); + dataMax = Math.max(dataMax, boundsRange.max); - if ( - modelPlotEnabled === true || - (contextForecastData !== undefined && contextForecastData.length > 0) - ) { - const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; - _.each(combinedData, item => { - boundsRange.min = Math.min(item.lower, boundsRange.min); - boundsRange.max = Math.max(item.upper, boundsRange.max); - }); - dataMin = Math.min(dataMin, boundsRange.min); - dataMax = Math.max(dataMax, boundsRange.max); - - // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. - if (valuesRange.max - valuesRange.min < 0.5 * (dataMax - dataMin)) { - if (valuesRange.min > dataMin) { - chartLimits.min = valuesRange.min - 0.5 * (valuesRange.max - valuesRange.min); - } - - if (valuesRange.max < dataMax) { - chartLimits.max = valuesRange.max + 0.5 * (valuesRange.max - valuesRange.min); - } + // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. + if (valuesRange.max - valuesRange.min < 0.5 * (dataMax - dataMin)) { + if (valuesRange.min > dataMin) { + chartLimits.min = valuesRange.min - 0.5 * (valuesRange.max - valuesRange.min); } - } - - this.contextYScale = d3.scale - .linear() - .range([cxtChartHeight, contextChartLineTopMargin]) - .domain([chartLimits.min, chartLimits.max]); - - const borders = cxtGroup.append('g').attr('class', 'axis'); - - // Add borders left and right. - borders - .append('line') - .attr('x1', 0) - .attr('y1', 0) - .attr('x2', 0) - .attr('y2', cxtChartHeight + swlHeight); - borders - .append('line') - .attr('x1', cxtWidth) - .attr('y1', 0) - .attr('x2', cxtWidth) - .attr('y2', cxtChartHeight + swlHeight); - - // Add x axis. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - const xAxis = d3.svg - .axis() - .scale(this.contextXScale) - .orient('top') - .innerTickSize(-cxtChartHeight) - .outerTickSize(0) - .tickPadding(0) - .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) - .tickFormat(d => { - return moment(d).format(xAxisTickFormat); - }); - - cxtGroup.datum(data); - const contextBoundsArea = d3.svg - .area() - .x(d => { - return this.contextXScale(d.date); - }) - .y0(d => { - return this.contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); - }) - .y1(d => { - return this.contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); - }) - .defined(d => d.lower !== null && d.upper !== null); - - if (modelPlotEnabled === true) { - cxtGroup - .append('path') - .datum(data) - .attr('class', 'area context') - .attr('d', contextBoundsArea); + if (valuesRange.max < dataMax) { + chartLimits.max = valuesRange.max + 0.5 * (valuesRange.max - valuesRange.min); + } } + } - const contextValuesLine = d3.svg - .line() - .x(d => { - return this.contextXScale(d.date); - }) - .y(d => { - return this.contextYScale(d.value); - }) - .defined(d => d.value !== null); + this.contextYScale = d3.scale + .linear() + .range([cxtChartHeight, contextChartLineTopMargin]) + .domain([chartLimits.min, chartLimits.max]); + + const borders = cxtGroup.append('g').attr('class', 'axis'); + + // Add borders left and right. + borders + .append('line') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 0) + .attr('y2', cxtChartHeight + swlHeight); + borders + .append('line') + .attr('x1', cxtWidth) + .attr('y1', 0) + .attr('x2', cxtWidth) + .attr('y2', cxtChartHeight + swlHeight); + + // Add x axis. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + const xAxis = d3.svg + .axis() + .scale(this.contextXScale) + .orient('top') + .innerTickSize(-cxtChartHeight) + .outerTickSize(0) + .tickPadding(0) + .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) + .tickFormat(d => { + return moment(d).format(xAxisTickFormat); + }); + cxtGroup.datum(data); + + const contextBoundsArea = d3.svg + .area() + .x(d => { + return this.contextXScale(d.date); + }) + .y0(d => { + return this.contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); + }) + .y1(d => { + return this.contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); + }) + .defined(d => d.lower !== null && d.upper !== null); + + if (modelPlotEnabled === true) { cxtGroup .append('path') .datum(data) - .attr('class', 'values-line') - .attr('d', contextValuesLine); - drawLineChartDots(data, cxtGroup, contextValuesLine, 1); - - // Create the path elements for the forecast value line and bounds area. - if (contextForecastData !== undefined) { - cxtGroup - .append('path') - .datum(contextForecastData) - .attr('class', 'area forecast') - .attr('d', contextBoundsArea); - cxtGroup - .append('path') - .datum(contextForecastData) - .attr('class', 'values-line forecast') - .attr('d', contextValuesLine); - } - - // Create and draw the anomaly swimlane. - const swimlane = cxtGroup - .append('g') - .attr('class', 'swimlane') - .attr('transform', 'translate(0,' + cxtChartHeight + ')'); - - this.drawSwimlane(swimlane, cxtWidth, swlHeight); - - // Draw a mask over the sections of the context chart and swimlane - // which fall outside of the zoom brush selection area. - this.mask = new ContextChartMask(cxtGroup, contextChartData, modelPlotEnabled, swlHeight) - .x(this.contextXScale) - .y(this.contextYScale); + .attr('class', 'area context') + .attr('d', contextBoundsArea); + } - // Draw the x axis on top of the mask so that the labels are visible. + const contextValuesLine = d3.svg + .line() + .x(d => { + return this.contextXScale(d.date); + }) + .y(d => { + return this.contextYScale(d.value); + }) + .defined(d => d.value !== null); + + cxtGroup + .append('path') + .datum(data) + .attr('class', 'values-line') + .attr('d', contextValuesLine); + drawLineChartDots(data, cxtGroup, contextValuesLine, 1); + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData !== undefined) { cxtGroup - .append('g') - .attr('class', 'x axis context-chart-axis') - .call(xAxis); - - // Move the x axis labels up so that they are inside the contact chart area. - cxtGroup.selectAll('.x.context-chart-axis text').attr('dy', cxtChartHeight - 5); - - filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); - - this.drawContextBrush(cxtGroup); + .append('path') + .datum(contextForecastData) + .attr('class', 'area forecast') + .attr('d', contextBoundsArea); + cxtGroup + .append('path') + .datum(contextForecastData) + .attr('class', 'values-line forecast') + .attr('d', contextValuesLine); } - drawContextBrush = contextGroup => { - const { contextChartSelected } = this.props; - - const brush = this.brush; - const contextXScale = this.contextXScale; - const mask = this.mask; - - // Create the brush for zooming in to the focus area of interest. - brush - .x(contextXScale) - .on('brush', brushing) - .on('brushend', brushed); - - contextGroup - .append('g') - .attr('class', 'x brush') - .call(brush) - .selectAll('rect') - .attr('y', -1) - .attr('height', contextChartHeight + swimlaneHeight + 1); - - // move the left and right resize areas over to - // be under the handles - contextGroup - .selectAll('.w rect') - .attr('x', -10) - .attr('width', 10); - - contextGroup - .selectAll('.e rect') - .attr('x', 0) - .attr('width', 10); - - const handleBrushExtent = brush.extent(); - - const topBorder = contextGroup - .append('rect') - .attr('class', 'top-border') - .attr('y', -2) - .attr('height', contextChartLineTopMargin); - - // Draw the brush handles using SVG foreignObject elements. - // Note these are not supported on IE11 and below, so will not appear in IE. - const leftHandle = contextGroup - .append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .attr('x', contextXScale(handleBrushExtent[0]) - 10) - .html( - '
' - ); - const rightHandle = contextGroup - .append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .attr('x', contextXScale(handleBrushExtent[1]) + 0) - .html( - '
' - ); + // Create and draw the anomaly swimlane. + const swimlane = cxtGroup + .append('g') + .attr('class', 'swimlane') + .attr('transform', 'translate(0,' + cxtChartHeight + ')'); - const showBrush = show => { - if (show === true) { - const brushExtent = brush.extent(); - mask.reveal(brushExtent); - leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); - rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); - - topBorder.attr('x', contextXScale(brushExtent[0]) + 1); - // Use Math.max(0, ...) to make sure we don't end up - // with a negative width which would cause an SVG error. - topBorder.attr( - 'width', - Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) - ); - } + this.drawSwimlane(swimlane, cxtWidth, swlHeight); - this.setBrushVisibility(show); - }; + // Draw a mask over the sections of the context chart and swimlane + // which fall outside of the zoom brush selection area. + this.mask = new ContextChartMask(cxtGroup, contextChartData, modelPlotEnabled, swlHeight) + .x(this.contextXScale) + .y(this.contextYScale); - showBrush(!brush.empty()); + // Draw the x axis on top of the mask so that the labels are visible. + cxtGroup + .append('g') + .attr('class', 'x axis context-chart-axis') + .call(xAxis); - function brushing() { - const isEmpty = brush.empty(); - showBrush(!isEmpty); - } - - const that = this; - function brushed() { - const isEmpty = brush.empty(); - - const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); - const selectionMin = selectedBounds[0].getTime(); - const selectionMax = selectedBounds[1].getTime(); + // Move the x axis labels up so that they are inside the contact chart area. + cxtGroup.selectAll('.x.context-chart-axis text').attr('dy', cxtChartHeight - 5); - // Avoid triggering an update if bounds haven't changed - if ( - that.selectedBounds !== undefined && - that.selectedBounds.min.valueOf() === selectionMin && - that.selectedBounds.max.valueOf() === selectionMax - ) { - return; - } + filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); - showBrush(!isEmpty); + this.drawContextBrush(cxtGroup); + } - // Set the color of the swimlane cells according to whether they are inside the selection. - contextGroup.selectAll('.swimlane-cell').style('fill', d => { - const cellMs = d.date.getTime(); - if (cellMs < selectionMin || cellMs > selectionMax) { - return anomalyGrayScale(d.score); - } else { - return anomalyColorScale(d.score); - } - }); + drawContextBrush = contextGroup => { + const { contextChartSelected } = this.props; + + const brush = this.brush; + const contextXScale = this.contextXScale; + const mask = this.mask; + + // Create the brush for zooming in to the focus area of interest. + brush + .x(contextXScale) + .on('brush', brushing) + .on('brushend', brushed); + + contextGroup + .append('g') + .attr('class', 'x brush') + .call(brush) + .selectAll('rect') + .attr('y', -1) + .attr('height', contextChartHeight + swimlaneHeight + 1); + + // move the left and right resize areas over to + // be under the handles + contextGroup + .selectAll('.w rect') + .attr('x', -10) + .attr('width', 10); + + contextGroup + .selectAll('.e rect') + .attr('x', 0) + .attr('width', 10); + + const handleBrushExtent = brush.extent(); + + const topBorder = contextGroup + .append('rect') + .attr('class', 'top-border') + .attr('y', -2) + .attr('height', contextChartLineTopMargin); + + // Draw the brush handles using SVG foreignObject elements. + // Note these are not supported on IE11 and below, so will not appear in IE. + const leftHandle = contextGroup + .append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .attr('x', contextXScale(handleBrushExtent[0]) - 10) + .html( + '
' + ); + const rightHandle = contextGroup + .append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .attr('x', contextXScale(handleBrushExtent[1]) + 0) + .html( + '
' + ); - that.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; - contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + const showBrush = show => { + if (show === true) { + const brushExtent = brush.extent(); + mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); + rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); + + topBorder.attr('x', contextXScale(brushExtent[0]) + 1); + // Use Math.max(0, ...) to make sure we don't end up + // with a negative width which would cause an SVG error. + topBorder.attr( + 'width', + Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) + ); } - }; - setBrushVisibility = show => { - const mask = this.mask; + this.setBrushVisibility(show); + }; - if (mask !== undefined) { - const visibility = show ? 'visible' : 'hidden'; - mask.style('visibility', visibility); + showBrush(!brush.empty()); - d3.selectAll('.brush').style('visibility', visibility); + function brushing() { + const isEmpty = brush.empty(); + showBrush(!isEmpty); + } - const brushHandles = d3.selectAll('.brush-handle-inner'); - brushHandles.style('visibility', visibility); + const that = this; + function brushed() { + const isEmpty = brush.empty(); - const topBorder = d3.selectAll('.top-border'); - topBorder.style('visibility', visibility); + const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); + const selectionMin = selectedBounds[0].getTime(); + const selectionMax = selectedBounds[1].getTime(); - const border = d3.selectAll('.chart-border-highlight'); - border.style('visibility', visibility); + // Avoid triggering an update if bounds haven't changed + if ( + that.selectedBounds !== undefined && + that.selectedBounds.min.valueOf() === selectionMin && + that.selectedBounds.max.valueOf() === selectionMax + ) { + return; } - }; - drawSwimlane = (swlGroup, swlWidth, swlHeight) => { - const { contextAggregationInterval, swimlaneData } = this.props; + showBrush(!isEmpty); - const data = swimlaneData; - - if (typeof data === 'undefined') { - return; - } + // Set the color of the swimlane cells according to whether they are inside the selection. + contextGroup.selectAll('.swimlane-cell').style('fill', d => { + const cellMs = d.date.getTime(); + if (cellMs < selectionMin || cellMs > selectionMax) { + return anomalyGrayScale(d.score); + } else { + return anomalyColorScale(d.score); + } + }); - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, so set the - // x-axis min to the start of the aggregation interval. - // Need to use the min(earliest) and max(earliest) of the context chart - // aggregation to align the axes of the chart and swimlane elements. - const xAxisDomain = this.calculateContextXAxisDomain(); - const x = d3.time - .scale() - .range([0, swlWidth]) - .domain(xAxisDomain); - - const y = d3.scale - .linear() - .range([swlHeight, 0]) - .domain([0, swlHeight]); - - const xAxis = d3.svg - .axis() - .scale(x) - .orient('bottom') - .innerTickSize(-swlHeight) - .outerTickSize(0); - - const yAxis = d3.svg - .axis() - .scale(y) - .orient('left') - .tickValues(y.domain()) - .innerTickSize(-swlWidth) - .outerTickSize(0); - - const axes = swlGroup.append('g'); - - axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + swlHeight + ')') - .call(xAxis); - - axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); - - const earliest = xAxisDomain[0].getTime(); - const latest = xAxisDomain[1].getTime(); - const swimlaneAggMs = contextAggregationInterval.asMilliseconds(); - let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); - if (cellWidth < 1) { - cellWidth = 1; - } + that.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; + contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + } + }; - const cells = swlGroup - .append('g') - .attr('class', 'swimlane-cells') - .selectAll('rect') - .data(data); + setBrushVisibility = show => { + const mask = this.mask; - cells - .enter() - .append('rect') - .attr('x', d => { - return x(d.date); - }) - .attr('y', 0) - .attr('rx', 0) - .attr('ry', 0) - .attr('class', d => { - return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden'; - }) - .attr('width', cellWidth) - .attr('height', swlHeight) - .style('fill', d => { - return anomalyColorScale(d.score); - }); - }; + if (mask !== undefined) { + const visibility = show ? 'visible' : 'hidden'; + mask.style('visibility', visibility); - calculateContextXAxisDomain = () => { - const { bounds, contextAggregationInterval, swimlaneData } = this.props; - // Calculates the x axis domain for the context elements. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - // Context chart and swimlane use the same aggregation interval. - let earliest = bounds.min.valueOf(); - - if (swimlaneData !== undefined && swimlaneData.length > 0) { - // Adjust the earliest back to the time of the first swimlane point - // if this is before the time filter minimum. - earliest = Math.min(_.first(swimlaneData).date.getTime(), bounds.min.valueOf()); - } + d3.selectAll('.brush').style('visibility', visibility); - const contextAggMs = contextAggregationInterval.asMilliseconds(); - const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; - const latestMs = Math.ceil(bounds.max.valueOf() / contextAggMs) * contextAggMs; + const brushHandles = d3.selectAll('.brush-handle-inner'); + brushHandles.style('visibility', visibility); - return [new Date(earliestMs), new Date(latestMs)]; - }; + const topBorder = d3.selectAll('.top-border'); + topBorder.style('visibility', visibility); - // Sets the extent of the brush on the context chart to the - // supplied from and to Date objects. - setContextBrushExtent = (from, to, fireEvent) => { - const brush = this.brush; - const brushExtent = brush.extent(); + const border = d3.selectAll('.chart-border-highlight'); + border.style('visibility', visibility); + } + }; - const newExtent = [from, to]; - if ( - newExtent[0].getTime() === brushExtent[0].getTime() && - newExtent[1].getTime() === brushExtent[1].getTime() - ) { - fireEvent = false; - } + drawSwimlane = (swlGroup, swlWidth, swlHeight) => { + const { contextAggregationInterval, swimlaneData } = this.props; - brush.extent(newExtent); - brush(d3.select('.brush')); - if (fireEvent) { - brush.event(d3.select('.brush')); - } - }; + const data = swimlaneData; - setZoomInterval(ms) { - const { bounds, zoomTo } = this.props; + if (typeof data === 'undefined') { + return; + } - const minBoundsMs = bounds.min.valueOf(); - const maxBoundsMs = bounds.max.valueOf(); + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, so set the + // x-axis min to the start of the aggregation interval. + // Need to use the min(earliest) and max(earliest) of the context chart + // aggregation to align the axes of the chart and swimlane elements. + const xAxisDomain = this.calculateContextXAxisDomain(); + const x = d3.time + .scale() + .range([0, swlWidth]) + .domain(xAxisDomain); + + const y = d3.scale + .linear() + .range([swlHeight, 0]) + .domain([0, swlHeight]); + + const xAxis = d3.svg + .axis() + .scale(x) + .orient('bottom') + .innerTickSize(-swlHeight) + .outerTickSize(0); + + const yAxis = d3.svg + .axis() + .scale(y) + .orient('left') + .tickValues(y.domain()) + .innerTickSize(-swlWidth) + .outerTickSize(0); + + const axes = swlGroup.append('g'); + + axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + swlHeight + ')') + .call(xAxis); + + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); + + const earliest = xAxisDomain[0].getTime(); + const latest = xAxisDomain[1].getTime(); + const swimlaneAggMs = contextAggregationInterval.asMilliseconds(); + let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); + if (cellWidth < 1) { + cellWidth = 1; + } - // Attempt to retain the same zoom end time. - // If not, go back to the bounds start and add on the required millis. - const millis = +ms; - let to = zoomTo.getTime(); - let from = to - millis; - if (from < minBoundsMs) { - from = minBoundsMs; - to = Math.min(minBoundsMs + millis, maxBoundsMs); - } + const cells = swlGroup + .append('g') + .attr('class', 'swimlane-cells') + .selectAll('rect') + .data(data); + + cells + .enter() + .append('rect') + .attr('x', d => { + return x(d.date); + }) + .attr('y', 0) + .attr('rx', 0) + .attr('ry', 0) + .attr('class', d => { + return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden'; + }) + .attr('width', cellWidth) + .attr('height', swlHeight) + .style('fill', d => { + return anomalyColorScale(d.score); + }); + }; + + calculateContextXAxisDomain = () => { + const { bounds, contextAggregationInterval, swimlaneData } = this.props; + // Calculates the x axis domain for the context elements. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + // Context chart and swimlane use the same aggregation interval. + let earliest = bounds.min.valueOf(); + + if (swimlaneData !== undefined && swimlaneData.length > 0) { + // Adjust the earliest back to the time of the first swimlane point + // if this is before the time filter minimum. + earliest = Math.min(_.first(swimlaneData).date.getTime(), bounds.min.valueOf()); + } - this.setContextBrushExtent(new Date(from), new Date(to), true); + const contextAggMs = contextAggregationInterval.asMilliseconds(); + const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; + const latestMs = Math.ceil(bounds.max.valueOf() / contextAggMs) * contextAggMs; + + return [new Date(earliestMs), new Date(latestMs)]; + }; + + // Sets the extent of the brush on the context chart to the + // supplied from and to Date objects. + setContextBrushExtent = (from, to, fireEvent) => { + const brush = this.brush; + const brushExtent = brush.extent(); + + const newExtent = [from, to]; + if ( + newExtent[0].getTime() === brushExtent[0].getTime() && + newExtent[1].getTime() === brushExtent[1].getTime() + ) { + fireEvent = false; } - showFocusChartTooltip(marker, circle) { - const { modelPlotEnabled, intl } = this.props; + brush.extent(newExtent); + brush(d3.select('.brush')); + if (fireEvent) { + brush.event(d3.select('.brush')); + } + }; + + setZoomInterval(ms) { + const { bounds, zoomTo } = this.props; + + const minBoundsMs = bounds.min.valueOf(); + const maxBoundsMs = bounds.max.valueOf(); + + // Attempt to retain the same zoom end time. + // If not, go back to the bounds start and add on the required millis. + const millis = +ms; + let to = zoomTo.getTime(); + let from = to - millis; + if (from < minBoundsMs) { + from = minBoundsMs; + to = Math.min(minBoundsMs + millis, maxBoundsMs); + } - const fieldFormat = this.fieldFormat; - const seriesKey = 'single_metric_viewer'; + this.setContextBrushExtent(new Date(from), new Date(to), true); + } - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTimeSeconds(marker.date); - const tooltipData = [{ name: formattedDate }]; + showFocusChartTooltip(marker, circle) { + const { modelPlotEnabled } = this.props; + + const fieldFormat = this.fieldFormat; + const seriesKey = 'single_metric_viewer'; + + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTimeSeconds(marker.date); + const tooltipData = [{ name: formattedDate }]; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: anomalyColorScale(score), + seriesKey, + yAccessor: 'anomaly_score', + }); - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + if (showMultiBucketAnomalyTooltip(marker) === true) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', - }), - value: displayScore, - color: anomalyColorScale(score), + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel', + { + defaultMessage: 'multi-bucket impact', + } + ), + value: getMultiBucketImpactLabel(marker.multiBucketImpact), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'multi_bucket_impact', }); + } - if (showMultiBucketAnomalyTooltip(marker) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel', - defaultMessage: 'multi-bucket impact', - }), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), - seriesKey, - yAccessor: 'multi_bucket_impact', - }); - } - - if (modelPlotEnabled === false) { - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && marker.function !== 'rare') { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.actualLabel', - defaultMessage: 'actual', - }), - value: formatValue(marker.actual, marker.function, fieldFormat), - seriesKey, - yAccessor: 'actual', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, marker.function, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } else { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { - const numberOfCauses = marker.numberOfCauses; - // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. - const byFieldName = mlEscape(marker.byFieldName); - tooltipData.push({ - name: intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel', - defaultMessage: '{numberOfCauses}{plusSign} unusual {byFieldName} values', - }, - { - numberOfCauses, - byFieldName, - // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. - plusSign: numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } - } else { + if (modelPlotEnabled === false) { + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && marker.function !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.actualLabel', { defaultMessage: 'actual', }), value: formatValue(marker.actual, marker.function, fieldFormat), @@ -1528,212 +1460,269 @@ const TimeseriesChartIntl = injectI18n( yAccessor: 'actual', }); tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', - defaultMessage: 'upper bounds', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel', { + defaultMessage: 'typical', }), - value: formatValue(marker.upper, marker.function, fieldFormat), + value: formatValue(marker.typical, marker.function, fieldFormat), seriesKey, - yAccessor: 'upper_bounds', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', - defaultMessage: 'lower bounds', - }), - value: formatValue(marker.lower, marker.function, fieldFormat), - seriesKey, - yAccessor: 'lower_bounds', - }); - } - } else { - // TODO - need better formatting for small decimals. - if (_.get(marker, 'isForecast', false) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel', - defaultMessage: 'prediction', - }), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesKey, - yAccessor: 'prediction', + yAccessor: 'typical', }); } else { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', { defaultMessage: 'value', }), value: formatValue(marker.value, marker.function, fieldFormat), seriesKey, yAccessor: 'value', }); + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + const numberOfCauses = marker.numberOfCauses; + // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + const byFieldName = mlEscape(marker.byFieldName); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel', + { + defaultMessage: '{numberOfCauses}{plusSign} unusual {byFieldName} values', + values: { + numberOfCauses, + byFieldName, + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + plusSign: numberOfCauses < 10 ? '' : '+', + }, + } + ), + seriesKey, + yAccessor: 'numberOfCauses', + }); + } } - - if (modelPlotEnabled === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel', + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', + { + defaultMessage: 'actual', + } + ), + value: formatValue(marker.actual, marker.function, fieldFormat), + seriesKey, + yAccessor: 'actual', + }); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', + { defaultMessage: 'upper bounds', - }), - value: formatValue(marker.upper, marker.function, fieldFormat), - seriesKey, - yAccessor: 'upper_bounds', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.lowerBoundsLabel', + } + ), + value: formatValue(marker.upper, marker.function, fieldFormat), + seriesKey, + yAccessor: 'upper_bounds', + }); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', + { defaultMessage: 'lower bounds', - }), - value: formatValue(marker.lower, marker.function, fieldFormat), - seriesKey, - yAccessor: 'lower_bounds', - }); - } + } + ), + value: formatValue(marker.lower, marker.function, fieldFormat), + seriesKey, + yAccessor: 'lower_bounds', + }); } - - if (_.has(marker, 'scheduledEvents')) { - marker.scheduledEvents.forEach((scheduledEvent, i) => { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', - defaultMessage: 'scheduled event{counter}', - }, - { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' } - ), - value: scheduledEvent, - seriesKey, - yAccessor: `scheduled_events_${i + 1}`, - }); + } else { + // TODO - need better formatting for small decimals. + if (_.get(marker, 'isForecast', false) === true) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel', + { + defaultMessage: 'prediction', + } + ), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesKey, + yAccessor: 'prediction', + }); + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - if (mlAnnotationsEnabled && _.has(marker, 'annotation')) { - tooltipData.length = 0; + if (modelPlotEnabled === true) { tooltipData.push({ - name: marker.annotation, + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel', + { + defaultMessage: 'upper bounds', + } + ), + value: formatValue(marker.upper, marker.function, fieldFormat), + seriesKey, + yAccessor: 'upper_bounds', }); - let timespan = moment(marker.timestamp).format('MMMM Do YYYY, HH:mm'); - - if (typeof marker.end_timestamp !== 'undefined') { - timespan += ` - ${moment(marker.end_timestamp).format('MMMM Do YYYY, HH:mm')}`; - } tooltipData.push({ - name: timespan, + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.lowerBoundsLabel', + { + defaultMessage: 'lower bounds', + } + ), + value: formatValue(marker.lower, marker.function, fieldFormat), + seriesKey, + yAccessor: 'lower_bounds', }); } + } - let xOffset = LINE_CHART_ANOMALY_RADIUS * 2; + if (_.has(marker, 'scheduledEvents')) { + marker.scheduledEvents.forEach((scheduledEvent, i) => { + tooltipData.push({ + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', { + defaultMessage: 'scheduled event{counter}', + values: { + counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '', + }, + }), + value: scheduledEvent, + seriesKey, + yAccessor: `scheduled_events_${i + 1}`, + }); + }); + } - // When the annotation area is hovered - if (circle.tagName.toLowerCase() === 'rect') { - const x = Number(circle.getAttribute('x')); - if (x < 0) { - // The beginning of the annotation area is outside of the focus chart, - // hence we need to adjust the x offset of a tooltip. - xOffset = Math.abs(x); - } - } + if (_.has(marker, 'annotation')) { + tooltipData.length = 0; + tooltipData.push({ + name: marker.annotation, + }); + let timespan = moment(marker.timestamp).format('MMMM Do YYYY, HH:mm'); - mlChartTooltipService.show(tooltipData, circle, { - x: xOffset, - y: 0, + if (typeof marker.end_timestamp !== 'undefined') { + timespan += ` - ${moment(marker.end_timestamp).format('MMMM Do YYYY, HH:mm')}`; + } + tooltipData.push({ + name: timespan, }); } - highlightFocusChartAnomaly(record) { - // Highlights the anomaly marker in the focus chart corresponding to the specified record. - - const { focusChartData, focusAggregationInterval } = this.props; + let xOffset = LINE_CHART_ANOMALY_RADIUS * 2; - const focusXScale = this.focusXScale; - const focusYScale = this.focusYScale; - const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + // When the annotation area is hovered + if (circle.tagName.toLowerCase() === 'rect') { + const x = Number(circle.getAttribute('x')); + if (x < 0) { + // The beginning of the annotation area is outside of the focus chart, + // hence we need to adjust the x offset of a tooltip. + xOffset = Math.abs(x); + } + } - // Find the anomaly marker which corresponds to the time of the anomaly record. - // Depending on the way the chart is aggregated, there may not be - // a point at exactly the same time as the record being highlighted. - const anomalyTime = record.source.timestamp; - const markerToSelect = findChartPointForAnomalyTime( - focusChartData, - anomalyTime, - focusAggregationInterval - ); + mlChartTooltipService.show(tooltipData, circle, { + x: xOffset, + y: 0, + }); + } - // Render an additional highlighted anomaly marker on the focus chart. - // TODO - plot anomaly markers for cases where there is an anomaly due - // to the absence of data and model plot is enabled. - if (markerToSelect !== undefined) { - const selectedMarker = d3 - .select('.focus-chart-markers') - .selectAll('.focus-chart-highlighted-marker') - .data([markerToSelect]); - if (showMultiBucketAnomalyMarker(markerToSelect) === true) { - selectedMarker - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) - .attr( - 'class', - d => - `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id} highlighted` - ); - } else { - selectedMarker - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .attr('cx', d => focusXScale(d.date)) - .attr('cy', d => focusYScale(d.value)) - .attr( - 'class', - d => - `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore).id} highlighted` - ); - } + highlightFocusChartAnomaly(record) { + // Highlights the anomaly marker in the focus chart corresponding to the specified record. + + const { focusChartData, focusAggregationInterval } = this.props; + + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + + // Find the anomaly marker which corresponds to the time of the anomaly record. + // Depending on the way the chart is aggregated, there may not be + // a point at exactly the same time as the record being highlighted. + const anomalyTime = record.source.timestamp; + const markerToSelect = findChartPointForAnomalyTime( + focusChartData, + anomalyTime, + focusAggregationInterval + ); + + // Render an additional highlighted anomaly marker on the focus chart. + // TODO - plot anomaly markers for cases where there is an anomaly due + // to the absence of data and model plot is enabled. + if (markerToSelect !== undefined) { + const selectedMarker = d3 + .select('.focus-chart-markers') + .selectAll('.focus-chart-highlighted-marker') + .data([markerToSelect]); + if (showMultiBucketAnomalyMarker(markerToSelect) === true) { + selectedMarker + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) + .attr( + 'class', + d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id} highlighted` + ); + } else { + selectedMarker + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .attr('cx', d => focusXScale(d.date)) + .attr('cy', d => focusYScale(d.value)) + .attr( + 'class', + d => `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore).id} highlighted` + ); + } - // Display the chart tooltip for this marker. - // Note the values of the record and marker may differ depending on the levels of aggregation. - const chartElement = d3.select(this.rootNode); - const anomalyMarker = chartElement.selectAll( - '.focus-chart-markers .anomaly-marker.highlighted' - ); - if (anomalyMarker.length) { - showFocusChartTooltip(markerToSelect, anomalyMarker[0][0]); - } + // Display the chart tooltip for this marker. + // Note the values of the record and marker may differ depending on the levels of aggregation. + const chartElement = d3.select(this.rootNode); + const anomalyMarker = chartElement.selectAll( + '.focus-chart-markers .anomaly-marker.highlighted' + ); + if (anomalyMarker.length) { + showFocusChartTooltip(markerToSelect, anomalyMarker[0][0]); } } + } - unhighlightFocusChartAnomaly() { - d3.select('.focus-chart-markers') - .selectAll('.anomaly-marker.highlighted') - .remove(); - mlChartTooltipService.hide(); - } + unhighlightFocusChartAnomaly() { + d3.select('.focus-chart-markers') + .selectAll('.anomaly-marker.highlighted') + .remove(); + mlChartTooltipService.hide(); + } - shouldComponentUpdate() { - return true; - } + shouldComponentUpdate() { + return true; + } - setRef(componentNode) { - this.rootNode = componentNode; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - render() { - return
; - } + render() { + return
; } -); +} export const TimeseriesChart = props => { const annotationProp = useObservable(annotation$); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js index cc77ad9f1a985..784ab102fd8ca 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js @@ -6,25 +6,12 @@ //import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; -import './timeseries_chart.test.mocks'; import moment from 'moment-timezone'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { TimeseriesChart } from './timeseries_chart'; -// mocking the following files because they import some core kibana -// code which the jest setup isn't happy with. -jest.mock('ui/chrome', () => ({ - addBasePath: path => path, - getBasePath: path => path, - // returns false for mlAnnotationsEnabled - getInjected: () => false, - getUiSettingsClient: () => ({ - get: jest.fn(), - }), -})); - jest.mock('../../../util/time_buckets', () => ({ TimeBuckets: function() { this.setBounds = jest.fn(); diff --git a/x-pack/legacy/plugins/ml/public/application/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 deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts +++ /dev/null @@ -1,9 +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('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index 4253316123f01..cb66b8d53e660 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -6,8 +6,6 @@ import { FC } from 'react'; -import { Timefilter } from 'ui/timefilter'; - import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils'; declare const TimeSeriesExplorer: FC<{ diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 6d9dbef64b009..ce52609f6d74f 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -30,8 +30,7 @@ import { EuiTitle, } from '@elastic/eui'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../util/dependency_cache'; import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; @@ -80,8 +79,6 @@ import { getFocusData, } from './timeseriesexplorer_utils'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be // obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values' @@ -135,8 +132,8 @@ function getTimeseriesexplorerDefaultState() { loading: false, modelPlotEnabled: false, // Toggles display of annotations in the focus chart - showAnnotations: mlAnnotationsEnabled, - showAnnotationsCheckbox: mlAnnotationsEnabled, + showAnnotations: true, + showAnnotationsCheckbox: true, // Toggles display of forecast data in the focus chart showForecast: true, showForecastCheckbox: false, @@ -216,11 +213,9 @@ export class TimeSeriesExplorer extends React.Component { }; toggleShowAnnotationsHandler = () => { - if (mlAnnotationsEnabled) { - this.setState(prevState => ({ - showAnnotations: !prevState.showAnnotations, - })); - } + this.setState(prevState => ({ + showAnnotations: !prevState.showAnnotations, + })); }; toggleShowForecastHandler = () => { @@ -815,6 +810,7 @@ export class TimeSeriesExplorer extends React.Component { }, } ); + const toastNotifications = getToastNotifications(); toastNotifications.addWarning(warningText); detectorIndex = detectors[0].index; } 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 index 03fe718de9bed..2a4eaf1a545a1 100644 --- 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 @@ -6,7 +6,6 @@ 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, @@ -26,8 +25,6 @@ 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; @@ -81,21 +78,19 @@ export function getFocusData( 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), + 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 }); + }) + ), // Plus query for forecast data if there is a forecastId stored in the appState. forecastId !== undefined ? (() => { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts index f1cdaf3ba8c1b..bd8f98e0428a1 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts @@ -8,7 +8,7 @@ import { difference, without } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; @@ -26,6 +26,7 @@ export function validateJobSelection( selectedJobIds: string[], setGlobalState: (...args: any) => void ) { + const toastNotifications = getToastNotifications(); const jobs = createTimeSeriesJobData(mlJobService.jobs); const timeSeriesJobIds: string[] = jobs.map((j: any) => j.id); 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 index dfa896b3124c6..568d078ae03b1 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -10,7 +10,7 @@ import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impa import moment from 'moment'; import rison from 'rison-node'; -import { timefilter } from 'ui/timefilter'; +import { getTimefilter } from '../util/dependency_cache'; import { CHART_TYPE } from '../explorer/explorer_constants'; @@ -180,6 +180,7 @@ export function getChartType(config) { 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 timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z const to = bounds.max.toISOString(); diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js index 437f71acb3376..4b33cb131be7f 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js @@ -6,7 +6,7 @@ import seriesConfig from '../explorer/explorer_charts/__mocks__/mock_series_config_filebeat'; -jest.mock('ui/timefilter', () => { +jest.mock('./dependency_cache', () => { const dateMath = require('@elastic/datemath'); let _time = undefined; const timefilter = { @@ -21,23 +21,11 @@ jest.mock('ui/timefilter', () => { }, }; return { - timefilter, + getTimefilter: () => timefilter, }; }); -import { timefilter } from 'ui/timefilter'; - -// A copy of these mocks for ui/chrome and ui/timefilter are also -// used in explorer_charts_container.test.js. -// TODO: Refactor the involved tests to avoid this duplication -jest.mock( - 'ui/chrome', - () => ({ - getBasePath: () => { - return ''; - }, - }), - { virtual: true } -); +import { getTimefilter } from './dependency_cache'; +const timefilter = getTimefilter(); import d3 from 'd3'; import moment from 'moment'; diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts new file mode 100644 index 0000000000000..52db6560b67f1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimefilterSetup } from 'src/plugins/data/public'; +import { + IUiSettingsClient, + ChromeStart, + SavedObjectsClientContract, + ApplicationStart, + HttpStart, +} from 'src/core/public'; +import { + IndexPatternsContract, + FieldFormatsStart, + DataPublicPluginStart, +} from 'src/plugins/data/public'; +import { + DocLinksStart, + ToastsStart, + OverlayStart, + ChromeRecentlyAccessed, + IBasePath, +} from 'kibana/public'; + +export interface DependencyCache { + timefilter: TimefilterSetup | null; + config: IUiSettingsClient | null; + indexPatterns: IndexPatternsContract | null; + chrome: ChromeStart | null; + docLinks: DocLinksStart | null; + toastNotifications: ToastsStart | null; + overlays: OverlayStart | null; + recentlyAccessed: ChromeRecentlyAccessed | null; + fieldFormats: FieldFormatsStart | null; + autocomplete: DataPublicPluginStart['autocomplete'] | null; + basePath: IBasePath | null; + savedObjectsClient: SavedObjectsClientContract | null; + XSRF: string | null; + APP_URL: string | null; + application: ApplicationStart | null; + http: HttpStart | null; +} + +const cache: DependencyCache = { + timefilter: null, + config: null, + indexPatterns: null, + chrome: null, + docLinks: null, + toastNotifications: null, + overlays: null, + recentlyAccessed: null, + fieldFormats: null, + autocomplete: null, + basePath: null, + savedObjectsClient: null, + XSRF: null, + APP_URL: null, + application: null, + http: null, +}; + +export function setDependencyCache(deps: Partial) { + cache.timefilter = deps.timefilter || null; + cache.config = deps.config || null; + cache.chrome = deps.chrome || null; + cache.indexPatterns = deps.indexPatterns || null; + cache.docLinks = deps.docLinks || null; + cache.toastNotifications = deps.toastNotifications || null; + cache.overlays = deps.overlays || null; + cache.recentlyAccessed = deps.recentlyAccessed || null; + cache.fieldFormats = deps.fieldFormats || null; + cache.autocomplete = deps.autocomplete || null; + cache.basePath = deps.basePath || null; + cache.savedObjectsClient = deps.savedObjectsClient || null; + cache.XSRF = deps.XSRF || null; + cache.APP_URL = deps.APP_URL || null; + cache.application = deps.application || null; + cache.http = deps.http || null; +} + +export function getTimefilter() { + if (cache.timefilter === null) { + throw new Error("timefilter hasn't been initialized"); + } + return cache.timefilter.timefilter; +} +export function getTimeHistory() { + if (cache.timefilter === null) { + throw new Error("timefilter hasn't been initialized"); + } + return cache.timefilter.history; +} + +export function getDocLinks() { + if (cache.docLinks === null) { + throw new Error("docLinks hasn't been initialized"); + } + return cache.docLinks; +} + +export function getToastNotifications() { + if (cache.toastNotifications === null) { + throw new Error("toast notifications haven't been initialized"); + } + return cache.toastNotifications; +} + +export function getOverlays() { + if (cache.overlays === null) { + throw new Error("overlays haven't been initialized"); + } + return cache.overlays; +} + +export function getUiSettings() { + if (cache.config === null) { + throw new Error("uiSettings hasn't been initialized"); + } + return cache.config; +} + +export function getRecentlyAccessed() { + if (cache.recentlyAccessed === null) { + throw new Error("recentlyAccessed hasn't been initialized"); + } + return cache.recentlyAccessed; +} + +export function getFieldFormats() { + if (cache.fieldFormats === null) { + throw new Error("fieldFormats hasn't been initialized"); + } + return cache.fieldFormats; +} + +export function getAutocomplete() { + if (cache.autocomplete === null) { + throw new Error("autocomplete hasn't been initialized"); + } + return cache.autocomplete; +} + +export function getChrome() { + if (cache.chrome === null) { + throw new Error("chrome hasn't been initialized"); + } + return cache.chrome; +} + +export function getBasePath() { + if (cache.basePath === null) { + throw new Error("basePath hasn't been initialized"); + } + return cache.basePath; +} + +export function getSavedObjectsClient() { + if (cache.savedObjectsClient === null) { + throw new Error("savedObjectsClient hasn't been initialized"); + } + return cache.savedObjectsClient; +} + +export function getXSRF() { + if (cache.XSRF === null) { + throw new Error("xsrf hasn't been initialized"); + } + return cache.XSRF; +} + +export function getAppUrl() { + if (cache.APP_URL === null) { + throw new Error("app url hasn't been initialized"); + } + return cache.APP_URL; +} + +export function getApplication() { + if (cache.application === null) { + throw new Error("application hasn't been initialized"); + } + return cache.application; +} + +export function getHttp() { + if (cache.http === null) { + throw new Error("http hasn't been initialized"); + } + return cache.http; +} + +export function clearCache() { + console.log('clearing dependency cache'); // eslint-disable-line no-console + Object.keys(cache).forEach(k => { + cache[k as keyof DependencyCache] = null; + }); +} diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 2e176b0044314..88b56b2329ae6 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { Query } from 'src/plugins/data/public'; import { IndexPattern, IIndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/public'; +import { getToastNotifications, getSavedObjectsClient } from './dependency_cache'; import { IndexPatternSavedObject, SavedSearchSavedObject } from '../../../common/types/kibana'; let indexPatternCache: IndexPatternSavedObject[] = []; @@ -21,7 +20,7 @@ let indexPatternsContract: IndexPatternsContract | null = null; export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { indexPatternsContract = indexPatterns; - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); return savedObjectsClient .find({ type: 'index-pattern', @@ -35,7 +34,7 @@ export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { } export function loadSavedSearches() { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); return savedObjectsClient .find({ type: 'search', @@ -48,7 +47,7 @@ export function loadSavedSearches() { } export async function loadSavedSearchById(id: string) { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); const ss = await savedObjectsClient.get('search', id); return ss.error === undefined ? ss : null; } @@ -122,6 +121,7 @@ export function getSavedSearchById(id: string): SavedSearchSavedObject | undefin export function timeBasedIndexCheck(indexPattern: IndexPattern, showNotification = false) { if (!indexPattern.isTimeBased()) { if (showNotification) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning({ title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', diff --git a/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts index 196d24bfff830..ab879e421cb09 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts @@ -6,9 +6,10 @@ // utility functions for managing which links get added to kibana's recently accessed list -import { npStart } from 'ui/new_platform'; import { i18n } from '@kbn/i18n'; +import { getRecentlyAccessed } from './dependency_cache'; + export function addItemToRecentlyAccessed(page: string, itemId: string, url: string) { let pageLabel = ''; let id = `ml-job-${itemId}`; @@ -37,6 +38,6 @@ export function addItemToRecentlyAccessed(page: string, itemId: string, url: str } url = `ml#/${page}/${url}`; - - npStart.core.chrome.recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); + const recentlyAccessed = getRecentlyAccessed(); + recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); } diff --git a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js index 2ac6f7dbd2fb5..ec1b8c842d204 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js @@ -7,20 +7,15 @@ import _ from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; import { parseInterval } from '../../../common/util/parse_interval'; +import { getFieldFormats, getUiSettings } from './dependency_cache'; 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. -const config = chrome.getUiSettingsClient(); - -const getConfig = (...args) => config.get(...args); - const calcAuto = timeBucketsCalcAutoIntervalProvider(); /** @@ -29,8 +24,9 @@ const calcAuto = timeBucketsCalcAutoIntervalProvider(); * for example the interval between points on a time series chart. */ export function TimeBuckets() { - this.barTarget = config.get('histogram:barTarget'); - this.maxBars = config.get('histogram:maxBars'); + const uiSettings = getUiSettings(); + this.barTarget = uiSettings.get('histogram:barTarget'); + this.maxBars = uiSettings.get('histogram:maxBars'); } /** @@ -301,8 +297,9 @@ TimeBuckets.prototype.getIntervalToNearestMultiple = function(divisorSecs) { * @return {string} */ TimeBuckets.prototype.getScaledDateFormat = function() { + const uiSettings = getUiSettings(); const interval = this.getInterval(); - const rules = config.get('dateFormat:scaled'); + const rules = uiSettings.get('dateFormat:scaled'); for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -311,17 +308,19 @@ TimeBuckets.prototype.getScaledDateFormat = function() { } } - return config.get('dateFormat'); + return uiSettings.get('dateFormat'); }; TimeBuckets.prototype.getScaledDateFormatter = function() { - const fieldFormats = npStart.plugins.data.fieldFormats; + const fieldFormats = getFieldFormats(); + const uiSettings = getUiSettings(); const DateFieldFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE); return new DateFieldFormat( { pattern: this.getScaledDateFormat(), }, - getConfig + // getConfig + uiSettings.get ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js similarity index 55% rename from x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js index dcb229e22e564..3f8f602e56d17 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js @@ -4,149 +4,163 @@ * you may not use this file except in compliance with the Elastic License. */ -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; import moment from 'moment'; -import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from '../time_buckets'; +import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from './time_buckets'; + +jest.mock( + './dependency_cache', + () => ({ + getUiSettings: () => { + return { + get(val) { + switch (val) { + case 'histogram:barTarget': + return 50; + case 'histogram:maxBars': + return 100; + } + }, + }; + }, + }), + { virtual: true } +); describe('ML - time buckets', () => { let autoBuckets; let customBuckets; beforeEach(() => { - ngMock.module('kibana'); - ngMock.inject(() => { - autoBuckets = new TimeBuckets(); - autoBuckets.setInterval('auto'); + autoBuckets = new TimeBuckets(); + autoBuckets.setInterval('auto'); - customBuckets = new TimeBuckets(); - customBuckets.setInterval('auto'); - customBuckets.setBarTarget(500); - customBuckets.setMaxBars(550); - }); + customBuckets = new TimeBuckets(); + customBuckets.setInterval('auto'); + customBuckets.setBarTarget(500); + customBuckets.setMaxBars(550); }); describe('default bar target', () => { - it('returns correct interval for default target with hour bounds', () => { + test('returns correct interval for default target with hour bounds', () => { const hourBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-01T01:00:00.000'), }; autoBuckets.setBounds(hourBounds); const hourResult = autoBuckets.getInterval(); - expect(hourResult.asSeconds()).to.be(60); // 1 minute + expect(hourResult.asSeconds()).toBe(60); // 1 minute }); - it('returns correct interval for default target with day bounds', () => { + test('returns correct interval for default target with day bounds', () => { const dayBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-02T00:00:00.000'), }; autoBuckets.setBounds(dayBounds); const dayResult = autoBuckets.getInterval(); - expect(dayResult.asSeconds()).to.be(1800); // 30 minutes + expect(dayResult.asSeconds()).toBe(1800); // 30 minutes }); - it('returns correct interval for default target with week bounds', () => { + test('returns correct interval for default target with week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-08T00:00:00.000'), }; autoBuckets.setBounds(weekBounds); const weekResult = autoBuckets.getInterval(); - expect(weekResult.asSeconds()).to.be(14400); // 4 hours + expect(weekResult.asSeconds()).toBe(14400); // 4 hours }); - it('returns correct interval for default target with 30 day bounds', () => { + test('returns correct interval for default target with 30 day bounds', () => { const monthBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-31T00:00:00.000'), }; autoBuckets.setBounds(monthBounds); const monthResult = autoBuckets.getInterval(); - expect(monthResult.asSeconds()).to.be(86400); // 1 day + expect(monthResult.asSeconds()).toBe(86400); // 1 day }); - it('returns correct interval for default target with year bounds', () => { + test('returns correct interval for default target with year bounds', () => { const yearBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2018-01-01T00:00:00.000'), }; autoBuckets.setBounds(yearBounds); const yearResult = autoBuckets.getInterval(); - expect(yearResult.asSeconds()).to.be(604800); // 1 week + expect(yearResult.asSeconds()).toBe(604800); // 1 week }); - it('returns correct interval as multiple of 3 hours for default target with 2 week bounds', () => { + test('returns correct interval as multiple of 3 hours for default target with 2 week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-15T00:00:00.000'), }; autoBuckets.setBounds(weekBounds); const weekResult = autoBuckets.getIntervalToNearestMultiple(10800); // 3 hours - expect(weekResult.asSeconds()).to.be(32400); // 9 hours + expect(weekResult.asSeconds()).toBe(32400); // 9 hours }); }); describe('custom bar target', () => { - it('returns correct interval for 500 bar target with hour bounds', () => { + test('returns correct interval for 500 bar target with hour bounds', () => { const hourBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-01T01:00:00.000'), }; customBuckets.setBounds(hourBounds); const hourResult = customBuckets.getInterval(); - expect(hourResult.asSeconds()).to.be(10); // 10 seconds + expect(hourResult.asSeconds()).toBe(10); // 10 seconds }); - it('returns correct interval for 500 bar target with day bounds', () => { + test('returns correct interval for 500 bar target with day bounds', () => { const dayBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-02T00:00:00.000'), }; customBuckets.setBounds(dayBounds); const dayResult = customBuckets.getInterval(); - expect(dayResult.asSeconds()).to.be(300); // 5 minutes + expect(dayResult.asSeconds()).toBe(300); // 5 minutes }); - it('returns correct interval for 500 bar target with week bounds', () => { + test('returns correct interval for 500 bar target with week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-08T00:00:00.000'), }; customBuckets.setBounds(weekBounds); const weekResult = customBuckets.getInterval(); - expect(weekResult.asSeconds()).to.be(1800); // 30 minutes + expect(weekResult.asSeconds()).toBe(1800); // 30 minutes }); - it('returns correct interval for 500 bar target with 30 day bounds', () => { + test('returns correct interval for 500 bar target with 30 day bounds', () => { const monthBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-31T00:00:00.000'), }; customBuckets.setBounds(monthBounds); const monthResult = customBuckets.getInterval(); - expect(monthResult.asSeconds()).to.be(7200); // 2 hours + expect(monthResult.asSeconds()).toBe(7200); // 2 hours }); - it('returns correct interval for 500 bar target with year bounds', () => { + test('returns correct interval for 500 bar target with year bounds', () => { const yearBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2018-01-01T00:00:00.000'), }; customBuckets.setBounds(yearBounds); const yearResult = customBuckets.getInterval(); - expect(yearResult.asSeconds()).to.be(86400); // 1 day + expect(yearResult.asSeconds()).toBe(86400); // 1 day }); - it('returns correct interval as multiple of 3 hours for 500 bar target with 90 day bounds', () => { + test('returns correct interval as multiple of 3 hours for 500 bar target with 90 day bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-04-01T00:00:00.000'), }; customBuckets.setBounds(weekBounds); const weekResult = customBuckets.getIntervalToNearestMultiple(10800); // 3 hours - expect(weekResult.asSeconds()).to.be(21600); // 6 hours + expect(weekResult.asSeconds()).toBe(21600); // 6 hours }); }); @@ -158,104 +172,104 @@ describe('ML - time buckets', () => { max: moment('2017-10-26T09:08:07.000+00:00'), }; - it('returns correct bounds for 4h interval without inclusive end', () => { + test('returns correct bounds for 4h interval without inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(4, 'hours'), false); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T08:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T11:59:59.999+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T08:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T11:59:59.999+00:00').valueOf()); }); - it('returns correct bounds for 4h interval with inclusive end', () => { + test('returns correct bounds for 4h interval with inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(4, 'hours'), true); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T08:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T12:00:00.000+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T08:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T12:00:00.000+00:00').valueOf()); }); - it('returns correct bounds for 1d interval without inclusive end', () => { + test('returns correct bounds for 1d interval without inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(1, 'days'), false); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T00:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T23:59:59.999+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T00:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T23:59:59.999+00:00').valueOf()); }); - it('returns correct bounds for 1d interval with inclusive end', () => { + test('returns correct bounds for 1d interval with inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(1, 'days'), true); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T00:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-27T00:00:00.000+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T00:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-27T00:00:00.000+00:00').valueOf()); }); }); describe('calcEsInterval', () => { - it('returns correct interval for various durations', () => { - expect(calcEsInterval(moment.duration(500, 'ms'))).to.eql({ + test('returns correct interval for various durations', () => { + expect(calcEsInterval(moment.duration(500, 'ms'))).toEqual({ value: 500, unit: 'ms', expression: '500ms', }); - expect(calcEsInterval(moment.duration(1000, 'ms'))).to.eql({ + expect(calcEsInterval(moment.duration(1000, 'ms'))).toEqual({ value: 1, unit: 's', expression: '1s', }); - expect(calcEsInterval(moment.duration(15, 's'))).to.eql({ + expect(calcEsInterval(moment.duration(15, 's'))).toEqual({ value: 15, unit: 's', expression: '15s', }); - expect(calcEsInterval(moment.duration(60, 's'))).to.eql({ + expect(calcEsInterval(moment.duration(60, 's'))).toEqual({ value: 1, unit: 'm', expression: '1m', }); - expect(calcEsInterval(moment.duration(1, 'm'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'm'))).toEqual({ value: 1, unit: 'm', expression: '1m', }); - expect(calcEsInterval(moment.duration(60, 'm'))).to.eql({ + expect(calcEsInterval(moment.duration(60, 'm'))).toEqual({ value: 1, unit: 'h', expression: '1h', }); - expect(calcEsInterval(moment.duration(3, 'h'))).to.eql({ + expect(calcEsInterval(moment.duration(3, 'h'))).toEqual({ value: 3, unit: 'h', expression: '3h', }); - expect(calcEsInterval(moment.duration(24, 'h'))).to.eql({ + expect(calcEsInterval(moment.duration(24, 'h'))).toEqual({ value: 1, unit: 'd', expression: '1d', }); - expect(calcEsInterval(moment.duration(3, 'd'))).to.eql({ + expect(calcEsInterval(moment.duration(3, 'd'))).toEqual({ value: 3, unit: 'd', expression: '3d', }); - expect(calcEsInterval(moment.duration(7, 'd'))).to.eql({ + expect(calcEsInterval(moment.duration(7, 'd'))).toEqual({ value: 1, unit: 'w', expression: '1w', }); - expect(calcEsInterval(moment.duration(1, 'w'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'w'))).toEqual({ value: 1, unit: 'w', expression: '1w', }); - expect(calcEsInterval(moment.duration(4, 'w'))).to.eql({ + expect(calcEsInterval(moment.duration(4, 'w'))).toEqual({ value: 28, unit: 'd', expression: '28d', }); - expect(calcEsInterval(moment.duration(1, 'M'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'M'))).toEqual({ value: 1, unit: 'M', expression: '1M', }); - expect(calcEsInterval(moment.duration(12, 'M'))).to.eql({ + expect(calcEsInterval(moment.duration(12, 'M'))).toEqual({ value: 1, unit: 'y', expression: '1y', }); - expect(calcEsInterval(moment.duration(1, 'y'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'y'))).toEqual({ value: 1, unit: 'y', expression: '1y', diff --git a/x-pack/legacy/plugins/ml/public/index.ts b/x-pack/legacy/plugins/ml/public/index.ts index 0057983104cc0..bafeb7277927f 100755 --- a/x-pack/legacy/plugins/ml/public/index.ts +++ b/x-pack/legacy/plugins/ml/public/index.ts @@ -5,8 +5,8 @@ */ import { PluginInitializer } from '../../../../../src/core/public'; -import { MlPlugin, MlPluginSetup, MlPluginStart } from './plugin'; +import { MlPlugin, Setup, Start } from './plugin'; -export const plugin: PluginInitializer = () => new MlPlugin(); +export const plugin: PluginInitializer = () => new MlPlugin(); -export { MlPluginSetup, MlPluginStart }; +export { Setup, Start }; diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index 3e007a18f4c5a..bf431f0986d68 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from '../../../../../src/core/public'; +import { PluginInitializerContext } from 'src/core/public'; import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, { - npData: npStart.plugins.data, + data: npStart.plugins.data, + __LEGACY: { + XSRF: chrome.getXsrfToken(), + // @ts-ignore getAppUrl is missing from chrome's definition + APP_URL: chrome.getAppUrl(), + }, }); export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index f68d1ffe88216..79af300bce4ec 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -4,15 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin as DataPlugin } from 'src/plugins/data/public'; -import { Plugin, CoreStart, CoreSetup } from '../../../../../src/core/public'; +import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; +import { MlDependencies } from './application/app'; -export interface MlSetupDependencies { - npData: ReturnType; -} - -export class MlPlugin implements Plugin { - setup(core: CoreSetup, { npData }: MlSetupDependencies) { +export class MlPlugin implements Plugin { + setup(core: CoreSetup, { data, __LEGACY }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -20,9 +16,11 @@ export class MlPlugin implements Plugin { const [coreStart, depsStart] = await core.getStartServices(); const { renderApp } = await import('./application/app'); return renderApp(coreStart, depsStart, { - ...params, - indexPatterns: npData.indexPatterns, - npData, + element: params.element, + appBasePath: params.appBasePath, + onAppLeave: params.onAppLeave, + data, + __LEGACY, }); }, }); @@ -30,11 +28,11 @@ export class MlPlugin implements Plugin { return {}; } - start(core: CoreStart, deps: {}) { + start(core: CoreStart, deps: any) { return {}; } public stop() {} } -export type MlPluginSetup = ReturnType; -export type MlPluginStart = ReturnType; +export type Setup = ReturnType; +export type Start = ReturnType; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts similarity index 57% rename from x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts rename to x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts index 156a42d9f3c50..dbd08eacd3ca2 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts +++ b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext } from 'react'; +import { IScopedClusterClient } from 'src/core/server'; -import { UiContext } from './ui_context'; - -export const useUiContext = () => { - return useContext(UiContext); -}; +export function isAnnotationsFeatureAvailable( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] +): boolean; diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js index d6440cae51666..186c27b0326d7 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js @@ -12,29 +12,24 @@ import { ML_ANNOTATIONS_INDEX_PATTERN, } from '../../../common/constants/index_patterns'; -import { FEATURE_ANNOTATIONS_ENABLED } from '../../../common/constants/feature_flags'; - // Annotations Feature is available if: -// - FEATURE_ANNOTATIONS_ENABLED is set to `true` // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present -export async function isAnnotationsFeatureAvailable(callWithRequest) { - if (!FEATURE_ANNOTATIONS_ENABLED) return false; - +export async function isAnnotationsFeatureAvailable(callAsCurrentUser) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; - const annotationsIndexExists = await callWithRequest('indices.exists', indexParams); + const annotationsIndexExists = await callAsCurrentUser('indices.exists', indexParams); if (!annotationsIndexExists) return false; - const annotationsReadAliasExists = await callWithRequest('indices.existsAlias', { + const annotationsReadAliasExists = await callAsCurrentUser('indices.existsAlias', { name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); if (!annotationsReadAliasExists) return false; - const annotationsWriteAliasExists = await callWithRequest('indices.existsAlias', { + const annotationsWriteAliasExists = await callAsCurrentUser('indices.existsAlias', { name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); if (!annotationsWriteAliasExists) return false; diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts index ea16eb8870014..7e0649d15bfb0 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -6,6 +6,7 @@ import getAnnotationsRequestMock from './__mocks__/get_annotations_request.json'; import getAnnotationsResponseMock from './__mocks__/get_annotations_response.json'; +import { RequestHandlerContext } from 'src/core/server'; import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../common/constants/index_patterns'; @@ -19,23 +20,30 @@ const acknowledgedResponseMock = { acknowledged: true }; const jobIdMock = 'jobIdMock'; describe('annotation_service', () => { - let callWithRequestSpy: jest.Mock; + let callWithRequestSpy: any; beforeEach(() => { - callWithRequestSpy = jest.fn((action: string) => { - switch (action) { - case 'delete': - case 'index': - return Promise.resolve(acknowledgedResponseMock); - case 'search': - return Promise.resolve(getAnnotationsResponseMock); - } - }); + callWithRequestSpy = ({ + ml: { + mlClient: { + callAsCurrentUser: jest.fn((action: string) => { + switch (action) { + case 'delete': + case 'index': + return Promise.resolve(acknowledgedResponseMock); + case 'search': + return Promise.resolve(getAnnotationsResponseMock); + } + }), + }, + }, + } as unknown) as RequestHandlerContext; }); describe('deleteAnnotation()', () => { it('should delete annotation', async done => { const { deleteAnnotation } = annotationServiceProvider(callWithRequestSpy); + const mockFunct = callWithRequestSpy.ml.mlClient.callAsCurrentUser; const annotationMockId = 'mockId'; const deleteParamsMock: DeleteParams = { @@ -46,8 +54,8 @@ describe('annotation_service', () => { const response = await deleteAnnotation(annotationMockId); - expect(callWithRequestSpy.mock.calls[0][0]).toBe('delete'); - expect(callWithRequestSpy.mock.calls[0][1]).toEqual(deleteParamsMock); + expect(mockFunct.mock.calls[0][0]).toBe('delete'); + expect(mockFunct.mock.calls[0][1]).toEqual(deleteParamsMock); expect(response).toBe(acknowledgedResponseMock); done(); }); @@ -56,6 +64,7 @@ describe('annotation_service', () => { describe('getAnnotation()', () => { it('should get annotations for specific job', async done => { const { getAnnotations } = annotationServiceProvider(callWithRequestSpy); + const mockFunct = callWithRequestSpy.ml.mlClient.callAsCurrentUser; const indexAnnotationArgsMock: IndexAnnotationArgs = { jobIds: [jobIdMock], @@ -66,8 +75,8 @@ describe('annotation_service', () => { const response: GetResponse = await getAnnotations(indexAnnotationArgsMock); - expect(callWithRequestSpy.mock.calls[0][0]).toBe('search'); - expect(callWithRequestSpy.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); + expect(mockFunct.mock.calls[0][0]).toBe('search'); + expect(mockFunct.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); expect(Object.keys(response.annotations)).toHaveLength(1); expect(response.annotations[jobIdMock]).toHaveLength(2); expect(isAnnotations(response.annotations[jobIdMock])).toBeTruthy(); @@ -81,9 +90,15 @@ describe('annotation_service', () => { message: 'mock error message', }; - const callWithRequestSpyError = jest.fn(() => { - return Promise.resolve(mockEsError); - }); + const callWithRequestSpyError = ({ + ml: { + mlClient: { + callAsCurrentUser: jest.fn(() => { + return Promise.resolve(mockEsError); + }), + }, + }, + } as unknown) as RequestHandlerContext; const { getAnnotations } = annotationServiceProvider(callWithRequestSpyError); @@ -103,6 +118,7 @@ describe('annotation_service', () => { describe('indexAnnotation()', () => { it('should index annotation', async done => { const { indexAnnotation } = annotationServiceProvider(callWithRequestSpy); + const mockFunct = callWithRequestSpy.ml.mlClient.callAsCurrentUser; const annotationMock: Annotation = { annotation: 'Annotation text', @@ -114,10 +130,10 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(callWithRequestSpy.mock.calls[0][0]).toBe('index'); + expect(mockFunct.mock.calls[0][0]).toBe('index'); // test if the annotation has been correctly augmented - const indexParamsCheck = callWithRequestSpy.mock.calls[0][1]; + const indexParamsCheck = mockFunct.mock.calls[0][1]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -130,6 +146,7 @@ describe('annotation_service', () => { it('should remove ._id and .key before updating annotation', async done => { const { indexAnnotation } = annotationServiceProvider(callWithRequestSpy); + const mockFunct = callWithRequestSpy.ml.mlClient.callAsCurrentUser; const annotationMock: Annotation = { _id: 'mockId', @@ -143,10 +160,10 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(callWithRequestSpy.mock.calls[0][0]).toBe('index'); + expect(mockFunct.mock.calls[0][0]).toBe('index'); // test if the annotation has been correctly augmented - const indexParamsCheck = callWithRequestSpy.mock.calls[0][1]; + const indexParamsCheck = mockFunct.mock.calls[0][1]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -161,6 +178,7 @@ describe('annotation_service', () => { it('should update annotation text and the username for modified_username', async done => { const { getAnnotations, indexAnnotation } = annotationServiceProvider(callWithRequestSpy); + const mockFunct = callWithRequestSpy.ml.mlClient.callAsCurrentUser; const indexAnnotationArgsMock: IndexAnnotationArgs = { jobIds: [jobIdMock], @@ -184,9 +202,9 @@ describe('annotation_service', () => { await indexAnnotation(annotation, modifiedUsernameMock); - expect(callWithRequestSpy.mock.calls[1][0]).toBe('index'); + expect(mockFunct.mock.calls[1][0]).toBe('index'); // test if the annotation has been correctly updated - const indexParamsCheck = callWithRequestSpy.mock.calls[1][1]; + const indexParamsCheck = mockFunct.mock.calls[1][1]; const modifiedAnnotation = indexParamsCheck.body; expect(modifiedAnnotation.annotation).toBe(modifiedAnnotationText); expect(modifiedAnnotation.create_username).toBe(originalUsernameMock); diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts index addcdcb376b93..399305ea2603e 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import _ from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; import { @@ -67,7 +68,8 @@ export type callWithRequestType = ( params: annotationProviderParams ) => Promise; -export function annotationProvider(callWithRequest: callWithRequestType) { +export function annotationProvider(context: RequestHandlerContext) { + const callAsCurrentUser = context.ml!.mlClient.callAsCurrentUser; async function indexAnnotation(annotation: Annotation, username: string) { if (isAnnotation(annotation) === false) { // No need to translate, this will not be exposed in the UI. @@ -94,7 +96,7 @@ export function annotationProvider(callWithRequest: callWithRequestType) { delete params.body.key; } - return await callWithRequest('index', params); + return await callAsCurrentUser('index', params); } async function getAnnotations({ @@ -213,7 +215,7 @@ export function annotationProvider(callWithRequest: callWithRequestType) { }; try { - const resp = await callWithRequest('search', params); + const resp = await callAsCurrentUser('search', params); if (resp.error !== undefined && resp.message !== undefined) { // No need to translate, this will not be exposed in the UI. @@ -252,7 +254,7 @@ export function annotationProvider(callWithRequest: callWithRequestType) { refresh: 'wait_for', }; - return await callWithRequest('delete', param); + return await callAsCurrentUser('delete', param); } return { diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts b/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts index a30ea572a2723..9847ce1db6552 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts +++ b/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { annotationProvider, callWithRequestType } from './annotation'; +import { RequestHandlerContext } from 'src/core/server'; +import { annotationProvider } from './annotation'; -export function annotationServiceProvider(callWithRequest: callWithRequestType) { +export function annotationServiceProvider(context: RequestHandlerContext) { return { - ...annotationProvider(callWithRequest), + ...annotationProvider(context), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts similarity index 79% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js rename to x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index 9c5048daeee3f..de23950e5cc1c 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -4,11 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; +import { RequestHandlerContext } from 'kibana/server'; +import { Module } from '../../../common/types/modules'; import { DataRecognizer } from '../data_recognizer'; describe('ML - data recognizer', () => { - const dr = new DataRecognizer({}); + const dr = new DataRecognizer(({ + ml: { + mlClient: { + callAsCurrentUser: jest.fn(), + }, + }, + core: { + savedObjects: { + client: { + find: jest.fn(), + bulkCreate: jest.fn(), + }, + }, + }, + } as unknown) as RequestHandlerContext); const moduleIds = [ 'apache_ecs', @@ -34,12 +49,12 @@ describe('ML - data recognizer', () => { it('listModules - check all module IDs', async () => { const modules = await dr.listModules(); const ids = modules.map(m => m.id); - expect(ids.join()).to.equal(moduleIds.join()); + expect(ids.join()).toEqual(moduleIds.join()); }); it('getModule - load a single module', async () => { const module = await dr.getModule(moduleIds[0]); - expect(module.id).to.equal(moduleIds[0]); + expect(module.id).toEqual(moduleIds[0]); }); describe('jobOverrides', () => { @@ -47,7 +62,7 @@ describe('ML - data recognizer', () => { // arrange const prefix = 'pre-'; const testJobId = 'test-job'; - const moduleConfig = { + const moduleConfig = ({ jobs: [ { id: `${prefix}${testJobId}`, @@ -64,7 +79,7 @@ describe('ML - data recognizer', () => { }, }, ], - }; + } as unknown) as Module; const jobOverrides = [ { analysis_limits: { @@ -80,7 +95,7 @@ describe('ML - data recognizer', () => { // act dr.applyJobConfigOverrides(moduleConfig, jobOverrides, prefix); // assert - expect(moduleConfig.jobs).to.eql([ + expect(moduleConfig.jobs).toEqual([ { config: { analysis_config: { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts similarity index 74% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.js rename to x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 1e7a72ee2750f..b62e44c299a2d 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -7,9 +7,25 @@ import fs from 'fs'; import Boom from 'boom'; import numeral from '@elastic/numeral'; -import { merge, get } from 'lodash'; +import { CallAPIOptions, RequestHandlerContext, SavedObjectsClientContract } from 'kibana/server'; +import { merge } from 'lodash'; +import { MlJob } from '../../../common/types/jobs'; +import { + KibanaObjects, + ModuleDataFeed, + ModuleJob, + Module, + JobOverride, + DatafeedOverride, + GeneralOverride, + DatafeedResponse, + JobResponse, + KibanaObjectResponse, + DataRecognizerConfigResponse, +} from '../../../common/types/modules'; import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; import { mlLog } from '../../client/log'; +// @ts-ignore import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; @@ -23,16 +39,90 @@ export const SAVED_OBJECT_TYPES = { VISUALIZATION: 'visualization', }; +interface RawModuleConfig { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: any; + jobs: Array<{ file: string; id: string }>; + datafeeds: Array<{ file: string; job_id: string; id: string }>; + kibana: { + search: Array<{ file: string; id: string }>; + visualization: Array<{ file: string; id: string }>; + dashboard: Array<{ file: string; id: string }>; + }; +} + +interface MlJobStats { + jobs: MlJob[]; +} + +interface Config { + dirName: any; + json: RawModuleConfig; +} + +interface Result { + id: string; + title: string; + query: any; + description: string; + logo: { icon: string } | null; +} + +interface JobStat { + id: string; + earliestTimestampMs: number; + latestTimestampMs: number; + latestResultsTimestampMs: number; +} + +interface JobExistResult { + jobsExist: boolean; + jobs: JobStat[]; +} + +interface ObjectExistResult { + id: string; + type: string; +} + +interface ObjectExistResponse { + id: string; + type: string; + exists: boolean; + savedObject?: any; +} + +interface SaveResults { + jobs: JobResponse[]; + datafeeds: DatafeedResponse[]; + savedObjects: KibanaObjectResponse[]; +} + export class DataRecognizer { - constructor(callWithRequest) { - this.callWithRequest = callWithRequest; - this.modulesDir = `${__dirname}/modules`; - this.savedObjectsClient = null; + modulesDir = `${__dirname}/modules`; + indexPatternName: string = ''; + indexPatternId: string | undefined = undefined; + savedObjectsClient: SavedObjectsClientContract; + + callAsCurrentUser: ( + endpoint: string, + clientParams?: Record, + options?: CallAPIOptions + ) => Promise; + + constructor(context: RequestHandlerContext) { + this.callAsCurrentUser = context.ml!.mlClient.callAsCurrentUser; + this.savedObjectsClient = context.core.savedObjects.client; } // list all directories under the given directory - async listDirs(dirName) { - const dirs = []; + async listDirs(dirName: string): Promise { + const dirs: string[] = []; return new Promise((resolve, reject) => { fs.readdir(dirName, (err, fileNames) => { if (err) { @@ -49,7 +139,7 @@ export class DataRecognizer { }); } - async readFile(fileName) { + async readFile(fileName: string): Promise { return new Promise((resolve, reject) => { fs.readFile(fileName, 'utf-8', (err, content) => { if (err) { @@ -61,12 +151,12 @@ export class DataRecognizer { }); } - async loadManifestFiles() { - const configs = []; + async loadManifestFiles(): Promise { + const configs: Config[] = []; const dirs = await this.listDirs(this.modulesDir); await Promise.all( dirs.map(async dir => { - let file; + let file: string | undefined; try { file = await this.readFile(`${this.modulesDir}/${dir}/manifest.json`); } catch (error) { @@ -90,15 +180,15 @@ export class DataRecognizer { } // get the manifest.json file for a specified id, e.g. "nginx" - async getManifestFile(id) { + async getManifestFile(id: string) { const manifestFiles = await this.loadManifestFiles(); return manifestFiles.find(i => i.json.id === id); } // called externally by an endpoint - async findMatches(indexPattern) { + async findMatches(indexPattern: string): Promise { const manifestFiles = await this.loadManifestFiles(); - const results = []; + const results: Result[] = []; await Promise.all( manifestFiles.map(async i => { @@ -138,7 +228,7 @@ export class DataRecognizer { return results; } - async searchForFields(moduleConfig, indexPattern) { + async searchForFields(moduleConfig: RawModuleConfig, indexPattern: string) { if (moduleConfig.query === undefined) { return false; } @@ -149,7 +239,7 @@ export class DataRecognizer { query: moduleConfig.query, }; - const resp = await this.callWithRequest('search', { + const resp = await this.callAsCurrentUser('search', { index, rest_total_hits_as_int: true, size, @@ -174,9 +264,9 @@ export class DataRecognizer { // called externally by an endpoint // supplying an optional prefix will add the prefix // to the job and datafeed configs - async getModule(id, prefix = '') { - let manifestJSON = null; - let dirName = null; + async getModule(id: string, prefix = ''): Promise { + let manifestJSON: RawModuleConfig | null = null; + let dirName: string | null = null; const manifestFile = await this.getManifestFile(id); if (manifestFile !== undefined) { @@ -186,9 +276,9 @@ export class DataRecognizer { throw Boom.notFound(`Module with the id "${id}" not found`); } - const jobs = []; - const datafeeds = []; - const kibana = {}; + const jobs: ModuleJob[] = []; + const datafeeds: ModuleDataFeed[] = []; + const kibana: KibanaObjects = {}; // load all of the job configs await Promise.all( manifestJSON.jobs.map(async job => { @@ -234,12 +324,12 @@ export class DataRecognizer { // load all of the kibana saved objects if (manifestJSON.kibana !== undefined) { - const kKeys = Object.keys(manifestJSON.kibana); + const kKeys = Object.keys(manifestJSON.kibana) as Array; await Promise.all( kKeys.map(async key => { kibana[key] = []; await Promise.all( - manifestJSON.kibana[key].map(async obj => { + manifestJSON!.kibana[key].map(async obj => { try { const kConfig = await this.readFile( `${this.modulesDir}/${dirName}/${KIBANA_DIR}/${key}/${obj.file}` @@ -247,7 +337,7 @@ export class DataRecognizer { // use the file name for the id const kId = obj.file.replace('.json', ''); const config = JSON.parse(kConfig); - kibana[key].push({ + kibana[key]!.push({ id: kId, title: config.title, config, @@ -276,21 +366,18 @@ export class DataRecognizer { // creates all of the jobs, datafeeds and savedObjects listed in the module config. // if any of the savedObjects already exist, they will not be overwritten. async setupModuleItems( - moduleId, - jobPrefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - request + moduleId: string, + jobPrefix: string, + groups: string[], + indexPatternName: string, + query: any, + useDedicatedIndex: boolean, + startDatafeed: boolean, + start: number, + end: number, + jobOverrides: JobOverride[], + datafeedOverrides: DatafeedOverride[] ) { - this.savedObjectsClient = request.getSavedObjectsClient(); - // load the config from disk const moduleConfig = await this.getModule(moduleId, jobPrefix); @@ -325,10 +412,10 @@ export class DataRecognizer { // create an empty results object const results = this.createResultsTemplate(moduleConfig); - const saveResults = { - jobs: [], - datafeeds: [], - savedObjects: [], + const saveResults: SaveResults = { + jobs: [] as JobResponse[], + datafeeds: [] as DatafeedResponse[], + savedObjects: [] as KibanaObjectResponse[], }; this.applyJobConfigOverrides(moduleConfig, jobOverrides, jobPrefix); @@ -395,8 +482,8 @@ export class DataRecognizer { return results; } - async dataRecognizerJobsExist(moduleId) { - const results = {}; + async dataRecognizerJobsExist(moduleId: string): Promise { + const results = {} as JobExistResult; // Load the module with the specified ID and check if the jobs // in the module have been created. @@ -405,7 +492,7 @@ export class DataRecognizer { // Add a wildcard at the front of each of the job IDs in the module, // as a prefix may have been supplied when creating the jobs in the module. const jobIds = module.jobs.map(job => `*${job.id}`); - const { jobsExist } = jobServiceProvider(this.callWithRequest); + const { jobsExist } = jobServiceProvider(this.callAsCurrentUser); const jobInfo = await jobsExist(jobIds); // Check if the value for any of the jobs is false. @@ -414,24 +501,24 @@ export class DataRecognizer { if (doJobsExist === true) { // Get the IDs of the jobs created from the module, and their earliest / latest timestamps. - const jobStats = await this.callWithRequest('ml.jobStats', { jobId: jobIds }); - const jobStatsJobs = []; + const jobStats: MlJobStats = await this.callAsCurrentUser('ml.jobStats', { jobId: jobIds }); + const jobStatsJobs: JobStat[] = []; if (jobStats.jobs && jobStats.jobs.length > 0) { const foundJobIds = jobStats.jobs.map(job => job.job_id); - const { getLatestBucketTimestampByJob } = resultsServiceProvider(this.callWithRequest); + const { getLatestBucketTimestampByJob } = resultsServiceProvider(this.callAsCurrentUser); const latestBucketTimestampsByJob = await getLatestBucketTimestampByJob(foundJobIds); jobStats.jobs.forEach(job => { const jobStat = { id: job.job_id, - }; + } as JobStat; if (job.data_counts) { jobStat.earliestTimestampMs = job.data_counts.earliest_record_timestamp; jobStat.latestTimestampMs = job.data_counts.latest_record_timestamp; jobStat.latestResultsTimestampMs = getLatestDataOrBucketTimestamp( jobStat.latestTimestampMs, - latestBucketTimestampsByJob[job.job_id] + latestBucketTimestampsByJob[job.job_id] as number ); } jobStatsJobs.push(jobStat); @@ -449,7 +536,7 @@ export class DataRecognizer { } // returns a id based on an index pattern name - async getIndexPatternId(name) { + async getIndexPatternId(name: string) { try { const indexPatterns = await this.loadIndexPatterns(); if (indexPatterns === undefined || indexPatterns.saved_objects === undefined) { @@ -466,16 +553,13 @@ export class DataRecognizer { // create a list of objects which are used to save the savedObjects. // each has an exists flag and those which do not already exist // contain a savedObject object which is sent to the server to save - async createSavedObjectsToSave(moduleConfig) { + async createSavedObjectsToSave(moduleConfig: Module) { // first check if the saved objects already exist. - const savedObjectExistResults = await this.checkIfSavedObjectsExist( - moduleConfig.kibana, - this.request - ); + const savedObjectExistResults = await this.checkIfSavedObjectsExist(moduleConfig.kibana); // loop through the kibanaSaveResults and update Object.keys(moduleConfig.kibana).forEach(type => { // type e.g. dashboard, search ,visualization - moduleConfig.kibana[type].forEach(configItem => { + moduleConfig.kibana[type]!.forEach(configItem => { const existsResult = savedObjectExistResults.find(o => o.id === configItem.id); if (existsResult !== undefined) { configItem.exists = existsResult.exists; @@ -495,25 +579,30 @@ export class DataRecognizer { } // update the exists flags in the kibana results - updateKibanaResults(kibanaSaveResults, objectExistResults) { - Object.keys(kibanaSaveResults).forEach(type => { - kibanaSaveResults[type].forEach(resultItem => { - const i = objectExistResults.find(o => o.id === resultItem.id && o.type === type); - resultItem.exists = i !== undefined; - }); - }); + updateKibanaResults( + kibanaSaveResults: DataRecognizerConfigResponse['kibana'], + objectExistResults: ObjectExistResult[] + ) { + (Object.keys(kibanaSaveResults) as Array).forEach( + type => { + kibanaSaveResults[type].forEach(resultItem => { + const i = objectExistResults.find(o => o.id === resultItem.id && o.type === type); + resultItem.exists = i !== undefined; + }); + } + ); } // loop through each type (dashboard, search, visualization) // load existing savedObjects for each type and compare to find out if // items with the same id already exist. // returns a flat list of objects with exists flags set - async checkIfSavedObjectsExist(kibanaObjects) { + async checkIfSavedObjectsExist(kibanaObjects: KibanaObjects): Promise { const types = Object.keys(kibanaObjects); - const results = await Promise.all( + const results: ObjectExistResponse[][] = await Promise.all( types.map(async type => { const existingObjects = await this.loadExistingSavedObjects(type); - return kibanaObjects[type].map(obj => { + return kibanaObjects[type]!.map(obj => { const existingObject = existingObjects.saved_objects.find( o => o.attributes && o.attributes.title === obj.title ); @@ -526,17 +615,17 @@ export class DataRecognizer { }) ); // merge all results - return [].concat(...results); + return ([] as ObjectExistResponse[]).concat(...results); } // find all existing savedObjects for a given type - loadExistingSavedObjects(type) { + loadExistingSavedObjects(type: string) { return this.savedObjectsClient.find({ type, perPage: 1000 }); } // save the savedObjects if they do not exist already - async saveKibanaObjects(objectExistResults) { - let results = { saved_objects: [] }; + async saveKibanaObjects(objectExistResults: ObjectExistResponse[]) { + let results = { saved_objects: [] as any[] }; const filteredSavedObjects = objectExistResults .filter(o => o.exists === false) .map(o => o.savedObject); @@ -553,7 +642,7 @@ export class DataRecognizer { // save the jobs. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveJobs(jobs) { + async saveJobs(jobs: ModuleJob[]): Promise { return await Promise.all( jobs.map(async job => { const jobId = job.id; @@ -568,15 +657,15 @@ export class DataRecognizer { ); } - async saveJob(job) { + async saveJob(job: ModuleJob) { const { id: jobId, config: body } = job; - return this.callWithRequest('ml.addJob', { jobId, body }); + return this.callAsCurrentUser('ml.addJob', { jobId, body }); } // save the datafeeds. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveDatafeeds(datafeeds) { + async saveDatafeeds(datafeeds: ModuleDataFeed[]) { return await Promise.all( datafeeds.map(async datafeed => { try { @@ -589,24 +678,32 @@ export class DataRecognizer { ); } - async saveDatafeed(datafeed) { + async saveDatafeed(datafeed: ModuleDataFeed) { const { id: datafeedId, config: body } = datafeed; - return this.callWithRequest('ml.addDatafeed', { datafeedId, body }); + return this.callAsCurrentUser('ml.addDatafeed', { datafeedId, body }); } - async startDatafeeds(datafeeds, start, end) { - const results = {}; + async startDatafeeds( + datafeeds: ModuleDataFeed[], + start: number, + end: number + ): Promise<{ [key: string]: DatafeedResponse }> { + const results = {} as { [key: string]: DatafeedResponse }; for (const datafeed of datafeeds) { results[datafeed.id] = await this.startDatafeed(datafeed, start, end); } return results; } - async startDatafeed(datafeed, start, end) { - const result = { started: false }; + async startDatafeed( + datafeed: ModuleDataFeed, + start: number | undefined, + end: number | undefined + ): Promise { + const result = { started: false } as DatafeedResponse; let opened = false; try { - const openResult = await this.callWithRequest('ml.openJob', { + const openResult = await this.callAsCurrentUser('ml.openJob', { jobId: datafeed.config.job_id, }); opened = openResult.opened; @@ -622,7 +719,7 @@ export class DataRecognizer { } if (opened) { try { - const duration = { start: 0 }; + const duration: { start: number; end?: number } = { start: 0 }; if (start !== undefined) { duration.start = start; } @@ -630,7 +727,7 @@ export class DataRecognizer { duration.end = end; } - await this.callWithRequest('ml.startDatafeed', { datafeedId: datafeed.id, ...duration }); + await this.callAsCurrentUser('ml.startDatafeed', { datafeedId: datafeed.id, ...duration }); result.started = true; } catch (error) { result.started = false; @@ -642,7 +739,7 @@ export class DataRecognizer { // merge all of the save results into one result object // which is returned from the endpoint - async updateResults(results, saveResults) { + async updateResults(results: DataRecognizerConfigResponse, saveResults: SaveResults) { // update job results results.jobs.forEach(j => { saveResults.jobs.forEach(j2 => { @@ -669,34 +766,40 @@ export class DataRecognizer { }); // update savedObjects results - Object.keys(results.kibana).forEach(category => { - results.kibana[category].forEach(item => { - const result = saveResults.savedObjects.find(o => o.id === item.id); - if (result !== undefined) { - item.exists = result.exists; - - if (result.error === undefined) { - item.success = true; - } else { - item.success = false; - item.error = result.error; + (Object.keys(results.kibana) as Array).forEach( + category => { + results.kibana[category].forEach(item => { + const result = saveResults.savedObjects.find(o => o.id === item.id); + if (result !== undefined) { + item.exists = result.exists; + + if (result.error === undefined) { + item.success = true; + } else { + item.success = false; + item.error = result.error; + } } - } - }); - }); + }); + } + ); } // creates an empty results object, // listing each job/datafeed/savedObject with a save success boolean - createResultsTemplate(moduleConfig) { - const results = {}; + createResultsTemplate(moduleConfig: Module): DataRecognizerConfigResponse { + const results: DataRecognizerConfigResponse = {} as DataRecognizerConfigResponse; const reducedConfig = { jobs: moduleConfig.jobs, datafeeds: moduleConfig.datafeeds, kibana: moduleConfig.kibana, }; - function createResultsItems(configItems, resultItems, index) { + function createResultsItems( + configItems: any[], + resultItems: any, + index: string | number + ): void { resultItems[index] = []; configItems.forEach(j => { resultItems[index].push({ @@ -706,22 +809,23 @@ export class DataRecognizer { }); } - Object.keys(reducedConfig).forEach(i => { + (Object.keys(reducedConfig) as Array).forEach(i => { if (Array.isArray(reducedConfig[i])) { - createResultsItems(reducedConfig[i], results, i); + createResultsItems(reducedConfig[i] as any[], results, i); } else { - results[i] = {}; + results[i] = {} as any; Object.keys(reducedConfig[i]).forEach(k => { - createResultsItems(reducedConfig[i][k], results[i], k); + createResultsItems((reducedConfig[i] as Module['kibana'])[k] as any[], results[i], k); }); } }); + return results; } // if an override index pattern has been specified, // update all of the datafeeds. - updateDatafeedIndices(moduleConfig) { + updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed const indexPatternNames = this.indexPatternName.includes(',') @@ -729,7 +833,7 @@ export class DataRecognizer { : [this.indexPatternName]; moduleConfig.datafeeds.forEach(df => { - const newIndices = []; + const newIndices: string[] = []; // the datafeed can contain indexes and indices const currentIndices = df.config.indexes !== undefined ? df.config.indexes : df.config.indices; @@ -749,12 +853,11 @@ export class DataRecognizer { delete df.config.indexes; df.config.indices = newIndices; }); - moduleConfig.datafeeds; } // loop through the custom urls in each job and replace the INDEX_PATTERN_ID // marker for the id of the specified index pattern - updateJobUrlIndexPatterns(moduleConfig) { + updateJobUrlIndexPatterns(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { moduleConfig.jobs.forEach(job => { // if the job has custom_urls @@ -763,7 +866,10 @@ export class DataRecognizer { job.config.custom_settings.custom_urls.forEach(cUrl => { const url = cUrl.url_value; if (url.match(INDEX_PATTERN_ID)) { - const newUrl = url.replace(new RegExp(INDEX_PATTERN_ID, 'g'), this.indexPatternId); + const newUrl = url.replace( + new RegExp(INDEX_PATTERN_ID, 'g'), + this.indexPatternId as string + ); // update the job's url cUrl.url_value = newUrl; } @@ -775,7 +881,7 @@ export class DataRecognizer { // check the custom urls in the module's jobs to see if they contain INDEX_PATTERN_ID // which needs replacement - doJobUrlsContainIndexPatternId(moduleConfig) { + doJobUrlsContainIndexPatternId(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { for (const job of moduleConfig.jobs) { // if the job has custom_urls @@ -793,20 +899,23 @@ export class DataRecognizer { // loop through each kibana saved object and replace any INDEX_PATTERN_ID and // INDEX_PATTERN_NAME markers for the id or name of the specified index pattern - updateSavedObjectIndexPatterns(moduleConfig) { + updateSavedObjectIndexPatterns(moduleConfig: Module) { if (moduleConfig.kibana) { Object.keys(moduleConfig.kibana).forEach(category => { - moduleConfig.kibana[category].forEach(item => { - let jsonString = item.config.kibanaSavedObjectMeta.searchSourceJSON; + moduleConfig.kibana[category]!.forEach(item => { + let jsonString = item.config.kibanaSavedObjectMeta!.searchSourceJSON; if (jsonString.match(INDEX_PATTERN_ID)) { - jsonString = jsonString.replace(new RegExp(INDEX_PATTERN_ID, 'g'), this.indexPatternId); - item.config.kibanaSavedObjectMeta.searchSourceJSON = jsonString; + jsonString = jsonString.replace( + new RegExp(INDEX_PATTERN_ID, 'g'), + this.indexPatternId as string + ); + item.config.kibanaSavedObjectMeta!.searchSourceJSON = jsonString; } if (category === SAVED_OBJECT_TYPES.VISUALIZATION) { // Look for any INDEX_PATTERN_NAME tokens in visualization visState, // as e.g. Vega visualizations reference the Elasticsearch index pattern directly. - let visStateString = item.config.visState; + let visStateString = String(item.config.visState); if (visStateString !== undefined && visStateString.match(INDEX_PATTERN_NAME)) { visStateString = visStateString.replace( new RegExp(INDEX_PATTERN_NAME, 'g'), @@ -822,21 +931,23 @@ export class DataRecognizer { // ensure the model memory limit for each job is not greater than // the max model memory setting for the cluster - async updateModelMemoryLimits(moduleConfig) { - const { limits } = await this.callWithRequest('ml.info'); + async updateModelMemoryLimits(moduleConfig: Module) { + const { limits } = await this.callAsCurrentUser('ml.info'); const maxMml = limits.max_model_memory_limit; if (maxMml !== undefined) { - const maxBytes = numeral(maxMml.toUpperCase()).value(); + // @ts-ignore + const maxBytes: number = numeral(maxMml.toUpperCase()).value(); if (Array.isArray(moduleConfig.jobs)) { moduleConfig.jobs.forEach(job => { - const mml = get(job, 'config.analysis_limits.model_memory_limit'); + const mml = job.config?.analysis_limits?.model_memory_limit; if (mml !== undefined) { - const mmlBytes = numeral(mml.toUpperCase()).value(); + // @ts-ignore + const mmlBytes: number = numeral(mml.toUpperCase()).value(); if (mmlBytes > maxBytes) { // if the job's mml is over the max, // so set the jobs mml to be the max - job.config.analysis_limits.model_memory_limit = maxMml; + job.config.analysis_limits!.model_memory_limit = maxMml; } } }); @@ -846,11 +957,11 @@ export class DataRecognizer { // check the kibana saved searches JSON in the module to see if they contain INDEX_PATTERN_ID // which needs replacement - doSavedObjectsContainIndexPatternId(moduleConfig) { + doSavedObjectsContainIndexPatternId(moduleConfig: Module) { if (moduleConfig.kibana) { for (const category of Object.keys(moduleConfig.kibana)) { - for (const item of moduleConfig.kibana[category]) { - const jsonString = item.config.kibanaSavedObjectMeta.searchSourceJSON; + for (const item of moduleConfig.kibana[category]!) { + const jsonString = item.config.kibanaSavedObjectMeta!.searchSourceJSON; if (jsonString.match(INDEX_PATTERN_ID)) { return true; } @@ -860,7 +971,7 @@ export class DataRecognizer { return false; } - applyJobConfigOverrides(moduleConfig, jobOverrides, jobPrefix = '') { + applyJobConfigOverrides(moduleConfig: Module, jobOverrides: JobOverride[], jobPrefix = '') { if (jobOverrides === undefined || jobOverrides === null) { return; } @@ -878,8 +989,8 @@ export class DataRecognizer { // separate all the overrides. // the overrides which don't contain a job id will be applied to all jobs in the module - const generalOverrides = []; - const jobSpecificOverrides = []; + const generalOverrides: GeneralOverride[] = []; + const jobSpecificOverrides: JobOverride[] = []; overrides.forEach(override => { if (override.job_id === undefined) { @@ -889,7 +1000,7 @@ export class DataRecognizer { } }); - function processArrayValues(source, update) { + function processArrayValues(source: any, update: any) { if (typeof source !== 'object' || typeof update !== 'object') { return; } @@ -935,7 +1046,11 @@ export class DataRecognizer { }); } - applyDatafeedConfigOverrides(moduleConfig, datafeedOverrides, jobPrefix = '') { + applyDatafeedConfigOverrides( + moduleConfig: Module, + datafeedOverrides: DatafeedOverride | DatafeedOverride[], + jobPrefix = '' + ) { if (datafeedOverrides !== undefined && datafeedOverrides !== null) { if (typeof datafeedOverrides !== 'object') { throw Boom.badRequest( @@ -950,8 +1065,8 @@ export class DataRecognizer { // separate all the overrides. // the overrides which don't contain a datafeed id or a job id will be applied to all jobs in the module - const generalOverrides = []; - const datafeedSpecificOverrides = []; + const generalOverrides: GeneralOverride[] = []; + const datafeedSpecificOverrides: DatafeedOverride[] = []; overrides.forEach(o => { if (o.datafeed_id === undefined && o.job_id === undefined) { generalOverrides.push(o); @@ -970,7 +1085,7 @@ export class DataRecognizer { datafeedSpecificOverrides.forEach(o => { // either a job id or datafeed id has been specified, so create a new id // containing either one plus the prefix - const tempId = o.datafeed_id !== undefined ? o.datafeed_id : o.job_id; + const tempId: string = String(o.datafeed_id !== undefined ? o.datafeed_id : o.job_id); const dId = prefixDatafeedId(tempId, jobPrefix); const datafeed = datafeeds.find(d => d.id === dId); diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/index.js b/x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/index.js rename to x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts index 5b154991f7cf0..555a58fbb5333 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts @@ -30,7 +30,7 @@ interface Influencer { fieldValue: any; } -export function resultsServiceProvider(client: RequestHandlerContext | (() => any)) { +export function resultsServiceProvider(client: RequestHandlerContext | ((...args: any[]) => any)) { const callAsCurrentUser = typeof client === 'object' ? client.ml!.mlClient.callAsCurrentUser : client; // Obtains data for the anomalies table, aggregating anomalies by day or hour as requested. @@ -298,7 +298,7 @@ export function resultsServiceProvider(client: RequestHandlerContext | (() => an // Obtains the latest bucket result timestamp by job ID. // Returns data over all jobs unless an optional list of job IDs of interest is supplied. // Returned response consists of latest bucket timestamps (ms since Jan 1 1970) against job ID - async function getLatestBucketTimestampByJob(jobIds = []) { + async function getLatestBucketTimestampByJob(jobIds: string[] = []) { const filter: object[] = [ { term: { diff --git a/x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts new file mode 100644 index 0000000000000..7d3d6aabb129c --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.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 { schema } from '@kbn/config-schema'; + +export const indexAnnotationSchema = { + timestamp: schema.number(), + end_timestamp: schema.number(), + annotation: schema.string(), + job_id: schema.string(), + type: schema.string(), + create_time: schema.maybe(schema.number()), + create_username: schema.maybe(schema.string()), + modified_time: schema.maybe(schema.number()), + modified_username: schema.maybe(schema.string()), + _id: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), +}; + +export const getAnnotationsSchema = { + jobIds: schema.arrayOf(schema.string()), + earliestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), + latestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), + maxAnnotations: schema.number(), +}; + +export const deleteAnnotationSchema = { annotationId: schema.string() }; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/modules.ts b/x-pack/legacy/plugins/ml/server/new_platform/modules.ts new file mode 100644 index 0000000000000..46b7e53c22a05 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/modules.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 { schema } from '@kbn/config-schema'; + +export const setupModuleBodySchema = schema.object({ + prefix: schema.maybe(schema.string()), + groups: schema.maybe(schema.arrayOf(schema.string())), + indexPatternName: schema.maybe(schema.string()), + query: schema.maybe(schema.any()), + useDedicatedIndex: schema.maybe(schema.boolean()), + startDatafeed: schema.maybe(schema.boolean()), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + jobOverrides: schema.maybe(schema.any()), + datafeedOverrides: schema.maybe(schema.any()), +}); + +export const getModuleIdParamSchema = (optional = false) => { + const stringType = schema.string(); + return { moduleId: optional ? schema.maybe(stringType) : stringType }; +}; 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 2b9219b2226f5..68ab88744278e 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -24,7 +24,6 @@ import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; // @ts-ignore: could not find declaration file for module import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; -import { FEATURE_ANNOTATIONS_ENABLED } from '../../common/constants/feature_flags'; import { LICENSE_TYPE } from '../../common/constants/license'; // @ts-ignore: could not find declaration file for module import { annotationRoutes } from '../routes/annotations'; @@ -107,6 +106,7 @@ export interface RouteInitialization { xpackMainPlugin: MlXpackMainPlugin; savedObjects?: SavedObjectsLegacyService; spacesPlugin: any; + securityPlugin: any; cloud?: CloudSetup; } export interface UsageInitialization { @@ -134,7 +134,7 @@ export class Plugin { public setup(core: MlCoreSetup, plugins: PluginsSetup) { const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http, injectUiAppVars } = core; + const { http } = core; const pluginId = this.pluginId; mirrorPluginStatus(xpackMainPlugin, plugins.ml); @@ -197,13 +197,6 @@ export class Plugin { ], }; - injectUiAppVars('ml', () => { - return { - kbnIndex: this.config.get('kibana.index'), - mlAnnotationsEnabled: FEATURE_ANNOTATIONS_ENABLED, - }; - }); - // Can access via new platform router's handler function 'context' parameter - context.ml.mlClient const mlClient = core.elasticsearch.createClient('ml', { plugins: [elasticsearchJsPlugin] }); http.registerRouteHandlerContext('ml', (context, request) => { @@ -220,6 +213,7 @@ export class Plugin { elasticsearchService: core.elasticsearch, xpackMainPlugin: plugins.xpackMain, spacesPlugin: plugins.spaces, + securityPlugin: plugins.security, }; const extendedRouteInitializationDeps: RouteInitialization = { @@ -246,7 +240,7 @@ export class Plugin { jobValidationRoutes(extendedRouteInitializationDeps); notificationRoutes(routeInitializationDeps); systemRoutes(extendedRouteInitializationDeps); - dataRecognizer(routeInitializationDeps); + dataRecognizer(extendedRouteInitializationDeps); dataVisualizerRoutes(routeInitializationDeps); calendars(routeInitializationDeps); fieldsService(routeInitializationDeps); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts index fd8ecba0b20df..32d829db7f81b 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts @@ -35,8 +35,8 @@ export const categoryDefinitionSchema = { export const maxAnomalyScoreSchema = { jobIds: schema.arrayOf(schema.string()), - earliestMs: schema.number(), - latestMs: schema.number(), + earliestMs: schema.maybe(schema.number()), + latestMs: schema.maybe(schema.number()), }; export const categoryExamplesSchema = { diff --git a/x-pack/legacy/plugins/ml/server/routes/annotations.js b/x-pack/legacy/plugins/ml/server/routes/annotations.js deleted file mode 100644 index e7cb38184dc18..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/annotations.js +++ /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 Boom from 'boom'; -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; -import { wrapError } from '../client/errors'; -import { annotationServiceProvider } from '../models/annotation_service'; - -import { ANNOTATION_USER_UNKNOWN } from '../../common/constants/annotations'; - -function getAnnotationsFeatureUnavailableErrorMessage() { - return Boom.badRequest( - i18n.translate('xpack.ml.routes.annotations.annotationsFeatureUnavailableErrorMessage', { - defaultMessage: - 'Index and aliases required for the annotations feature have not been' + - ' created or are not accessible for the current user.', - }) - ); -} -export function annotationRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/annotations', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAnnotations } = annotationServiceProvider(callWithRequest); - return getAnnotations(request.payload).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'PUT', - path: '/api/ml/annotations/index', - async handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(callWithRequest); - if (annotationsFeatureAvailable === false) { - return getAnnotationsFeatureUnavailableErrorMessage(); - } - - const { indexAnnotation } = annotationServiceProvider(callWithRequest); - const username = _.get(request, 'auth.credentials.username', ANNOTATION_USER_UNKNOWN); - return indexAnnotation(request.payload, username).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'DELETE', - path: '/api/ml/annotations/delete/{annotationId}', - async handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(callWithRequest); - if (annotationsFeatureAvailable === false) { - return getAnnotationsFeatureUnavailableErrorMessage(); - } - - const annotationId = request.params.annotationId; - const { deleteAnnotation } = annotationServiceProvider(callWithRequest); - return deleteAnnotation(annotationId).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/annotations.ts b/x-pack/legacy/plugins/ml/server/routes/annotations.ts new file mode 100644 index 0000000000000..20f52b4b051c4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/annotations.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; +import { annotationServiceProvider } from '../models/annotation_service'; +import { wrapError } from '../client/error_wrapper'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + deleteAnnotationSchema, + getAnnotationsSchema, + indexAnnotationSchema, +} from '../new_platform/annotations_schema'; + +import { ANNOTATION_USER_UNKNOWN } from '../../common/constants/annotations'; + +function getAnnotationsFeatureUnavailableErrorMessage() { + return Boom.badRequest( + i18n.translate('xpack.ml.routes.annotations.annotationsFeatureUnavailableErrorMessage', { + defaultMessage: + 'Index and aliases required for the annotations feature have not been' + + ' created or are not accessible for the current user.', + }) + ); +} +/** + * Routes for annotations + */ +export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: RouteInitialization) { + /** + * @apiGroup Annotations + * + * @api {post} /api/ml/annotations Gets annotations + * @apiName GetAnnotations + * @apiDescription Gets annotations. + * + * @apiParam {String[]} jobIds List of job IDs + * @apiParam {String} earliestMs + * @apiParam {Number} latestMs + * @apiParam {Number} maxAnnotations Max limit of annotations returned + * + * @apiSuccess {Boolean} success + * @apiSuccess {Object} annotations + */ + router.post( + { + path: '/api/ml/annotations', + validate: { + body: schema.object(getAnnotationsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAnnotations } = annotationServiceProvider(context); + const resp = await getAnnotations(request.body); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup Annotations + * + * @api {put} /api/ml/annotations/index Index annotation + * @apiName IndexAnnotations + * @apiDescription Index the annotation. + * + * @apiParam {Object} annotation + * @apiParam {String} username + */ + router.put( + { + path: '/api/ml/annotations/index', + validate: { + body: schema.object(indexAnnotationSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( + context.ml!.mlClient.callAsCurrentUser + ); + if (annotationsFeatureAvailable === false) { + throw getAnnotationsFeatureUnavailableErrorMessage(); + } + + const { indexAnnotation } = annotationServiceProvider(context); + const user = securityPlugin.authc.getCurrentUser(request) || {}; + const resp = await indexAnnotation(request.body, user.username || ANNOTATION_USER_UNKNOWN); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup Annotations + * + * @api {delete} /api/ml/annotations/index Deletes annotation + * @apiName DeleteAnnotation + * @apiDescription Deletes specified annotation + * + * @apiParam {String} annotationId + */ + router.delete( + { + path: '/api/ml/annotations/delete/{annotationId}', + validate: { + params: schema.object(deleteAnnotationSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( + context.ml!.mlClient.callAsCurrentUser + ); + if (annotationsFeatureAvailable === false) { + throw getAnnotationsFeatureUnavailableErrorMessage(); + } + + const annotationId = request.params.annotationId; + const { deleteAnnotation } = annotationServiceProvider(context); + const resp = await deleteAnnotation(annotationId); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 1be31e2316228..919592f8ed62a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -35,12 +35,21 @@ "GetCategories", "FileDataVisualizer", "AnalyzeFile", - "ImportFile" + "ImportFile", "ResultsService", "GetAnomaliesTableData", "GetCategoryDefinition", "GetMaxAnomalyScore", "GetCategoryExamples", - "GetPartitionFieldsValues" + "GetPartitionFieldsValues", + "DataRecognizer", + "RecognizeIndex", + "GetModule", + "SetupModule", + "CheckExistingModuleJobs", + "Annotations", + "GetAnnotations", + "IndexAnnotations", + "DeleteAnnotation" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/modules.js b/x-pack/legacy/plugins/ml/server/routes/modules.js deleted file mode 100644 index e7d7b76aa7133..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/modules.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. - */ - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { DataRecognizer } from '../models/data_recognizer'; - -function recognize(callWithRequest, indexPatternTitle) { - const dr = new DataRecognizer(callWithRequest); - return dr.findMatches(indexPatternTitle); -} - -function getModule(callWithRequest, moduleId) { - const dr = new DataRecognizer(callWithRequest); - if (moduleId === undefined) { - return dr.listModules(); - } else { - return dr.getModule(moduleId); - } -} - -function saveModuleItems( - callWithRequest, - moduleId, - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - request -) { - const dr = new DataRecognizer(callWithRequest); - return dr.setupModuleItems( - moduleId, - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - request - ); -} - -function dataRecognizerJobsExist(callWithRequest, moduleId) { - const dr = new DataRecognizer(callWithRequest); - return dr.dataRecognizerJobsExist(moduleId); -} - -export function dataRecognizer({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'GET', - path: '/api/ml/modules/recognize/{indexPatternTitle}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const indexPatternTitle = request.params.indexPatternTitle; - return recognize(callWithRequest, indexPatternTitle).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/modules/get_module/{moduleId?}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - let moduleId = request.params.moduleId; - if (moduleId === '') { - // if the endpoint is called with a trailing / - // the moduleId will be an empty string. - moduleId = undefined; - } - return getModule(callWithRequest, moduleId).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/modules/setup/{moduleId}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const moduleId = request.params.moduleId; - - const { - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - } = request.payload; - - return saveModuleItems( - callWithRequest, - moduleId, - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - request - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/modules/jobs_exist/{moduleId}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const moduleId = request.params.moduleId; - return dataRecognizerJobsExist(callWithRequest, moduleId).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/modules.ts b/x-pack/legacy/plugins/ml/server/routes/modules.ts new file mode 100644 index 0000000000000..a40fb1c9149ca --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/modules.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandlerContext } from 'kibana/server'; +import { DatafeedOverride, JobOverride } from '../../common/types/modules'; +import { wrapError } from '../client/error_wrapper'; +import { DataRecognizer } from '../models/data_recognizer'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { getModuleIdParamSchema, setupModuleBodySchema } from '../new_platform/modules'; +import { RouteInitialization } from '../new_platform/plugin'; + +function recognize(context: RequestHandlerContext, indexPatternTitle: string) { + const dr = new DataRecognizer(context); + return dr.findMatches(indexPatternTitle); +} + +function getModule(context: RequestHandlerContext, moduleId: string) { + const dr = new DataRecognizer(context); + if (moduleId === undefined) { + return dr.listModules(); + } else { + return dr.getModule(moduleId); + } +} + +function saveModuleItems( + context: RequestHandlerContext, + moduleId: string, + prefix: string, + groups: string[], + indexPatternName: string, + query: any, + useDedicatedIndex: boolean, + startDatafeed: boolean, + start: number, + end: number, + jobOverrides: JobOverride[], + datafeedOverrides: DatafeedOverride[] +) { + const dr = new DataRecognizer(context); + return dr.setupModuleItems( + moduleId, + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + datafeedOverrides + ); +} + +function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: string) { + const dr = new DataRecognizer(context); + return dr.dataRecognizerJobsExist(moduleId); +} + +/** + * Recognizer routes. + */ +export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup DataRecognizer + * + * @api {get} /api/ml/modules/recognize/:indexPatternTitle Recognize index pattern + * @apiName RecognizeIndex + * @apiDescription Returns the list of modules that matching the index pattern. + * + * @apiParam {String} indexPatternTitle Index pattern title. + */ + router.get( + { + path: '/api/ml/modules/recognize/{indexPatternTitle}', + validate: { + params: schema.object({ + indexPatternTitle: schema.string(), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { indexPatternTitle } = request.params; + const results = await recognize(context, indexPatternTitle); + + return response.ok({ body: results }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup DataRecognizer + * + * @api {get} /api/ml/modules/get_module/:moduleId Get module + * @apiName GetModule + * @apiDescription Returns module by id. + * + * @apiParam {String} [moduleId] Module id + */ + router.get( + { + path: '/api/ml/modules/get_module/{moduleId?}', + validate: { + params: schema.object({ + ...getModuleIdParamSchema(true), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + let { moduleId } = request.params; + if (moduleId === '') { + // if the endpoint is called with a trailing / + // the moduleId will be an empty string. + moduleId = undefined; + } + const results = await getModule(context, moduleId); + + return response.ok({ body: results }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup DataRecognizer + * + * @api {post} /api/ml/modules/setup/:moduleId Setup module + * @apiName SetupModule + * @apiDescription Created module items. + * + * @apiParam {String} moduleId Module id + */ + router.post( + { + path: '/api/ml/modules/setup/{moduleId}', + validate: { + params: schema.object({ + ...getModuleIdParamSchema(), + }), + body: setupModuleBodySchema, + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { moduleId } = request.params; + + const { + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + datafeedOverrides, + } = request.body; + + const result = await saveModuleItems( + context, + moduleId, + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + datafeedOverrides + ); + + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup DataRecognizer + * + * @api {post} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist + * @apiName CheckExistingModuleJobs + * @apiDescription Checks if the jobs in the module have been created. + * + * @apiParam {String} moduleId Module id + */ + router.get( + { + path: '/api/ml/modules/jobs_exist/{moduleId}', + validate: { + params: schema.object({ + ...getModuleIdParamSchema(), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { moduleId } = request.params; + const result = await dataRecognizerJobsExist(context, moduleId); + + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js index 699a364433b3e..05f81f5c376a7 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/ops_buffer/ops_buffer.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../../../../common/constants'; import { EventRoller } from './event_roller'; import { CloudDetector } from '../../../cloud'; @@ -13,7 +12,7 @@ import { CloudDetector } from '../../../cloud'; * @param {Object} server HapiJS server instance * @return {Object} the revealed `push` and `flush` modules */ -export function opsBuffer({ config, log, getOSInfo }) { +export function opsBuffer({ config, getOSInfo }) { // determine the cloud service in the background const cloudDetector = new CloudDetector(); @@ -26,7 +25,6 @@ export function opsBuffer({ config, log, getOSInfo }) { return { push(event) { eventRoller.addEvent(event); - log(['debug', LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG], 'Received Kibana Ops event data'); }, hasEvents() { 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 bf141a9f0a0bf..7bb7b9f4da5d1 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 @@ -35,6 +35,8 @@ import { } from '../../../screens/hosts/events'; import { DEFAULT_TIMEOUT } from '../../lib/util/helpers'; +import { clearSearchBar } from '../../../tasks/header'; + const defaultHeadersInDefaultEcsCategory = [ { id: '@timestamp' }, { id: 'message' }, @@ -133,11 +135,15 @@ describe('Events Viewer', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); openEvents(); + waitsForEventsToBeLoaded(); + }); + + afterEach(() => { + clearSearchBar(); }); 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 - waitsForEventsToBeLoaded(); cy.get(HEADER_SUBTITLE) .invoke('text') .then(initialNumberOfEvents => { 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 fbf75e8a854c6..d410a89cf0723 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 @@ -47,9 +47,9 @@ describe('toggle column in timeline', () => { 'exist' ); - cy.get( - `[data-test-subj="timeline"] [data-test-subj="toggle-field-${timestampField}"]` - ).uncheck({ force: true }); + cy.get(`[data-test-subj="timeline"] [data-test-subj="toggle-field-${timestampField}"]`, { + timeout: DEFAULT_TIMEOUT, + }).uncheck({ force: true }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${timestampField}"]`).should( 'not.exist' diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index 33ee2cb1cb302..cbd1b2a074a59 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -55,28 +55,32 @@ describe('url state', () => { .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }).type( - `{selectall}{backspace}${ABSOLUTE_DATE_RANGE.newStartTimeTyped}` - ); + cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }) + .clear() + .type(`${ABSOLUTE_DATE_RANGE.newStartTimeTyped}`); cy.get(DATE_PICKER_APPLY_BUTTON, { timeout: DEFAULT_TIMEOUT }) .click({ force: true }) - .invoke('text') + .invoke('text', { timeout: DEFAULT_TIMEOUT }) .should('not.equal', 'Updating'); + cy.get('[data-test-subj="table-topNFlowSource-loading-false"]', { + timeout: DEFAULT_TIMEOUT, + }).should('exist'); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_TAB) .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }).type( - `{selectall}{backspace}${ABSOLUTE_DATE_RANGE.newEndTimeTyped}` - ); + cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }) + .clear() + .type(`${ABSOLUTE_DATE_RANGE.newEndTimeTyped}`); cy.get(DATE_PICKER_APPLY_BUTTON, { timeout: DEFAULT_TIMEOUT }) .click({ force: true }) - .invoke('text') + .invoke('text', { timeout: DEFAULT_TIMEOUT }) .should('not.equal', 'Updating'); cy.url().should( diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts b/x-pack/legacy/plugins/siem/cypress/tasks/header.ts index 96412b1eb6a3c..1405f4bd81848 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/header.ts @@ -4,6 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KQL_INPUT } from '../screens/header'; +import { DEFAULT_TIMEOUT } from '../tasks/login'; + export const navigateFromHeaderTo = (page: string) => { cy.get(page).click({ force: true }); }; + +export const clearSearchBar = () => { + cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }) + .clear() + .type('{enter}'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts index c78eb8f73f650..d30e49a25bab0 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts @@ -22,7 +22,7 @@ export const clearFieldsBrowser = () => { }; export const filterFieldsBrowser = (fieldName: string) => { - cy.get(FIELDS_BROWSER_FILTER_INPUT) + cy.get(FIELDS_BROWSER_FILTER_INPUT, { timeout: DEFAULT_TIMEOUT }) .type(fieldName) .should('not.have.class', 'euiFieldSearch-isLoading'); }; diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts index c91a2f6625194..8389d86fd2072 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/ui.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +export const MONITOR_ROUTE = '/monitor/:monitorId?'; + +export const OVERVIEW_ROUTE = '/'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 2fd4c762cf45f..13309acd03622 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -7,7 +7,6 @@ export { PingHistogram } from './charts/ping_histogram'; export { Snapshot } from './charts/snapshot_container'; export { KueryBar } from './kuerybar/kuery_bar_container'; -export { OverviewPage } from './pages/overview_container'; export { FilterGroup } from './filter_group/filter_group_container'; export { MonitorStatusDetails } from './monitor/status_details_container'; export { MonitorStatusBar } from './monitor/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.ts b/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.ts rename to x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.tsx new file mode 100644 index 0000000000000..9429b87061ff7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/pages/page_header_container.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 { connect } from 'react-redux'; +import { selectSelectedMonitor } from '../../../state/selectors'; +import { AppState } from '../../../state'; +import { PageHeaderComponent } from '../../../pages/page_header'; + +const mapStateToProps = (state: AppState) => ({ + monitorStatus: selectSelectedMonitor(state), +}); + +export const PageHeader = connect(mapStateToProps, null)(PageHeaderComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx index 331b5c9c0b096..f8e885147b992 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx @@ -6,10 +6,9 @@ import React from 'react'; import DateMath from '@elastic/datemath'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorChartsComponent } from '../monitor_charts'; import { MonitorChart } from '../../../../common/graphql/types'; -import { renderWithRouter } from '../../../lib'; +import { shallowWithRouter } from '../../../lib'; describe('MonitorCharts component', () => { let dateMathSpy: any; @@ -63,18 +62,16 @@ describe('MonitorCharts component', () => { }; it('renders the component without errors', () => { - const component = shallowWithIntl( - renderWithRouter( - - ) + const component = shallowWithRouter( + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx index 5ce88f2bd5c22..445d9302e3a9d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx @@ -4,19 +4,18 @@ * 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 { UptimeDatePicker } from '../uptime_date_picker'; -import { renderWithRouter } from '../../../lib'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; describe('UptimeDatePicker component', () => { it('validates props with shallow render', () => { - const component = shallowWithIntl(renderWithRouter()); + const component = shallowWithRouter(); expect(component).toMatchSnapshot(); }); it('renders properly with mock data', () => { - const component = renderWithIntl(renderWithRouter()); + const component = renderWithRouter(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx index c3e98134e438d..5d4e112aa5f28 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx @@ -5,9 +5,8 @@ */ import React from 'react'; -import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; -import { renderWithRouter } from '../../../../lib'; +import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; describe('MonitorBarSeries component', () => { @@ -161,13 +160,13 @@ describe('MonitorBarSeries component', () => { }); it('shallow renders a series when there are down items', () => { - const component = shallowWithIntl(renderWithRouter()); + const component = shallowWithRouter(); expect(component).toMatchSnapshot(); }); it('shallow renders null when there are no down items', () => { props.histogramSeries = []; - const component = shallowWithIntl(renderWithRouter()); + const component = shallowWithRouter(); expect(component).toEqual({}); }); @@ -189,20 +188,20 @@ describe('MonitorBarSeries component', () => { up: 0, }, ]; - const component = shallowWithIntl(renderWithRouter()); + const component = shallowWithRouter(); expect(component).toEqual({}); }); it('shallow renders nothing if the data series is null', () => { - const component = shallowWithIntl( - renderWithRouter() + const component = shallowWithRouter( + ); expect(component).toEqual({}); }); it('renders if the data series is present', () => { - const component = renderWithIntl( - renderWithRouter() + const component = renderWithRouter( + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx index 0c0393cb4fedf..1813229a97d1b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx @@ -5,9 +5,8 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { FilterStatusButton, FilterStatusButtonProps } from '../filter_status_button'; -import { renderWithRouter } from '../../../../lib/'; +import { shallowWithRouter } from '../../../../lib/'; describe('FilterStatusButton', () => { let props: FilterStatusButtonProps; @@ -21,7 +20,7 @@ describe('FilterStatusButton', () => { }); it('renders without errors for valid props', () => { - const wrapper = shallowWithIntl(renderWithRouter()); + const wrapper = shallowWithRouter(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap index ed50bc0be382a..d782eb565ef99 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap @@ -42,15 +42,15 @@ exports[`LocationMap component doesnt shows warning if geo is provided 1`] = ` } /> - - - - + + `; @@ -127,15 +127,15 @@ exports[`LocationMap component renders correctly against snapshot 1`] = ` } /> - - @@ -155,8 +155,8 @@ exports[`LocationMap component renders correctly against snapshot 1`] = ` } /> - - + + `; @@ -186,15 +186,15 @@ exports[`LocationMap component renders named locations that have missing geo dat } /> - - @@ -203,8 +203,8 @@ exports[`LocationMap component renders named locations that have missing geo dat upPoints={Array []} /> - - + + `; @@ -247,15 +247,15 @@ exports[`LocationMap component shows warning if geo information is missing 1`] = } /> - - @@ -271,8 +271,8 @@ exports[`LocationMap component shows warning if geo information is missing 1`] = } /> - - + + `; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx index 8b9e410b0de79..27fe3a2274270 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx @@ -32,7 +32,6 @@ const EuiFlexItemTags = styled(EuiFlexItem)` padding-top: 5px; @media (max-width: 850px) { order: 1; - text-align: center; } `; @@ -80,14 +79,14 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => { - - + + {isGeoInfoMissing && } - - + + ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx index bb9ce59ea62b1..14e91b9db920e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { MonitorSummaryResult } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; @@ -110,16 +110,14 @@ describe('MonitorList component', () => { }); it('renders the monitor list', () => { - const component = renderWithIntl( - renderWithRouter( - - ) + const component = renderWithRouter( + ); expect(component).toMatchSnapshot(); 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 index 9bd407902cb55..c222728df3bb3 100644 --- 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 @@ -5,11 +5,10 @@ */ 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'; import { MonitorDetails } from '../../../../../../common/runtime_types'; -import { renderWithRouter } from '../../../../../lib'; +import { shallowWithRouter } from '../../../../../lib'; describe('MonitorListDrawer component', () => { let summary: MonitorSummary; @@ -52,41 +51,35 @@ describe('MonitorListDrawer component', () => { }); it('renders nothing when no summary data is present', () => { - const component = shallowWithIntl( - renderWithRouter( - - ) + const component = shallowWithRouter( + ); expect(component).toEqual({}); }); it('renders nothing when no check data is present', () => { delete summary.state.checks; - const component = shallowWithIntl( - renderWithRouter( - - ) + const component = shallowWithRouter( + ); expect(component).toEqual({}); }); it('renders a MonitorListDrawer when there is only one check', () => { - const component = shallowWithIntl( - renderWithRouter( - - ) + const component = shallowWithRouter( + ); expect(component).toMatchSnapshot(); }); @@ -116,14 +109,12 @@ describe('MonitorListDrawer component', () => { }, ]; summary.state.checks = checks; - const component = shallowWithIntl( - renderWithRouter( - - ) + const component = shallowWithRouter( + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap index a2c52f9405289..827c9257893ad 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap @@ -11,7 +11,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = ` "entries": Array [ Object { "hash": "", - "key": "test", + "key": "TestKeyForTesting", "pathname": "/", "search": "?g=%22%22&dateRangeStart=now-12&dateRangeEnd=now&pagination=foo", "state": undefined, @@ -25,7 +25,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = ` "listen": [Function], "location": Object { "hash": "", - "key": "test", + "key": "TestKeyForTesting", "pathname": "/", "search": "?g=%22%22&dateRangeStart=now-12&dateRangeEnd=now&pagination=foo", "state": undefined, diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx index c9ba7b9bc0098..da6b33bc49300 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import DateMath from '@elastic/datemath'; import React, { useState, Fragment } from 'react'; import { useUrlParams, UptimeUrlParamsHook } from '../use_url_params'; import { UptimeRefreshContext } from '../../contexts'; -import { renderWithRouter } from '../../lib'; +import { mountWithRouter } from '../../lib'; import { createMemoryHistory } from 'history'; interface MockUrlParamsComponentProps { @@ -51,13 +50,11 @@ describe('useUrlParams', () => { const history = createMemoryHistory(); jest.spyOn(history, 'push'); - const component = mountWithIntl( - renderWithRouter( - - - , - history - ) + const component = mountWithRouter( + + + , + history ); const setUrlParamsButton = component.find('#setUrlParams'); @@ -69,17 +66,15 @@ describe('useUrlParams', () => { }); it('gets the expected values using the context', () => { - const component = mountWithIntl( - renderWithRouter( - - - - ) + const component = mountWithRouter( + + + ); const getUrlParamsButton = component.find('#getUrlParams'); @@ -95,18 +90,16 @@ describe('useUrlParams', () => { history.location.key = 'test'; jest.spyOn(history, 'push'); - const component = mountWithIntl( - renderWithRouter( - - - , - history - ) + const component = mountWithRouter( + + + , + history ); const getUrlParamsButton = component.find('#getUrlParams'); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/render_with_router.tsx b/x-pack/legacy/plugins/uptime/public/lib/helper/render_with_router.tsx index 5cd9ec23a3587..74d6cbf0a5a97 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/render_with_router.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/render_with_router.tsx @@ -4,18 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; import { createMemoryHistory } from 'history'; +import { mountWithIntl, renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -export const renderWithRouter = (Component: any, customHistory?: MemoryHistory) => { +const helperWithRouter: ( + helper: (node: ReactElement) => R, + component: ReactElement, + customHistory?: MemoryHistory +) => R = (helper, component, customHistory) => { if (customHistory) { - return {Component}; + customHistory.location.key = 'TestKeyForTesting'; + return helper({component}); } const history = createMemoryHistory(); history.location.key = 'TestKeyForTesting'; - return {Component}; + return helper({component}); +}; + +export const renderWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => { + return helperWithRouter(renderWithIntl, component, customHistory); +}; + +export const shallowWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => { + return helperWithRouter(shallowWithIntl, component, customHistory); +}; + +export const mountWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => { + return helperWithRouter(mountWithIntl, component, customHistory); }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/index.ts b/x-pack/legacy/plugins/uptime/public/lib/index.ts index 9a78c6df5d63d..07a0792360044 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/index.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { renderWithRouter } from './helper/render_with_router'; +export { renderWithRouter, shallowWithRouter, mountWithRouter } from './helper/render_with_router'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap new file mode 100644 index 0000000000000..6064caa868bf8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap new file mode 100644 index 0000000000000..a4d13963aaf77 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotFoundPage render component for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap new file mode 100644 index 0000000000000..fff947bd96024 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap new file mode 100644 index 0000000000000..2563b15eed5d5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap @@ -0,0 +1,1154 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageHeaderComponent mount expected page title for valid monitor route 1`] = ` + + + + +
+ +
+ +

+ https://www.elastic.co +

+
+
+
+ +
+ + + +
+ +
+ + } + > +
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="QuickSelectPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + +
+
+ + + +
+
+
+
+
+
+ } + iconType={false} + isCustom={true} + startDateControl={
} + > +
+ +
+ + +
+
+ +
+ + +
+ + + + + + + + + +
+
+
+ + + +
+ +
+ + +
+ + + + +`; + +exports[`PageHeaderComponent renders expected elements for valid props 1`] = ` +Array [ +
+
+

+ Overview +

+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+
, +
, +] +`; + +exports[`PageHeaderComponent renders expected title for valid monitor route 1`] = ` +Array [ +
+
+

+ https://www.elastic.co +

+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+
, +
, +] +`; + +exports[`PageHeaderComponent renders expected title for valid overview route 1`] = ` +Array [ +
+
+

+ Overview +

+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+
, +
, +] +`; + +exports[`PageHeaderComponent shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx new file mode 100644 index 0000000000000..8a1256c741c85 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { MonitorPage } from '../monitor'; +import { shallowWithRouter } from '../../lib'; + +describe('MonitorPage', () => { + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx new file mode 100644 index 0000000000000..2b6c60efc84b0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.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 React from 'react'; +import { shallowWithRouter } from '../../lib'; +import { NotFoundPage } from '../not_found'; + +describe('NotFoundPage', () => { + it('render component for valid props', () => { + const component = shallowWithRouter(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx new file mode 100644 index 0000000000000..365e96788bbbf --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.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. + */ + +import React from 'react'; +import { OverviewPageComponent } from '../overview'; +import { shallowWithRouter } from '../../lib'; + +describe('MonitorPage', () => { + const indexPattern = { + fields: [ + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + + { + name: 'monitor.check_group', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.duration.us', + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.id', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.ip', + type: 'ip', + esTypes: ['ip'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.name', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.status', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.timespan', + type: 'unknown', + esTypes: ['date_range'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'monitor.type', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ], + title: 'heartbeat-8*', + }; + + const autocomplete = { + getQuerySuggestions: jest.fn(), + hasQuerySuggestions: () => true, + getValueSuggestions: jest.fn(), + }; + + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx new file mode 100644 index 0000000000000..38d074cdb5dba --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { Route } from 'react-router-dom'; +import { PageHeaderComponent } from '../page_header'; +import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../lib'; +import { MONITOR_ROUTE, OVERVIEW_ROUTE } from '../../../common/constants'; +import { Ping } from '../../../common/graphql/types'; +import { createMemoryHistory } from 'history'; +import { ChromeBreadcrumb } from 'kibana/public'; + +describe('PageHeaderComponent', () => { + const monitorStatus: Ping = { + id: 'elastic-co', + tcp: { rtt: { connect: { us: 174982 } } }, + http: { + response: { + body: { + bytes: 2092041, + hash: '5d970606a6be810ae5d37115c4807fdd07ba4c3e407924ee5297e172d2efb3dc', + }, + status_code: 200, + }, + rtt: { + response_header: { us: 340175 }, + write_request: { us: 38 }, + validate: { us: 1797839 }, + content: { us: 1457663 }, + total: { us: 2030012 }, + }, + }, + monitor: { + ip: '2a04:4e42:3::729', + status: 'up', + duration: { us: 2030035 }, + type: 'http', + id: 'elastic-co', + name: 'elastic', + check_group: '2a017afa-4736-11ea-b3d0-acde48001122', + }, + resolve: { ip: '2a04:4e42:3::729', rtt: { us: 2102 } }, + url: { port: 443, full: 'https://www.elastic.co', scheme: 'https', domain: 'www.elastic.co' }, + ecs: { version: '1.4.0' }, + tls: { + certificate_not_valid_after: '2020-07-16T03:15:39.000Z', + rtt: { handshake: { us: 57115 } }, + certificate_not_valid_before: '2019-08-16T01:40:25.000Z', + }, + observer: { + geo: { name: 'US-West', location: '37.422994, -122.083666' }, + }, + timestamp: '2020-02-04T10:07:42.142Z', + }; + + it('shallow renders expected elements for valid props', () => { + const component = shallowWithRouter(); + expect(component).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + const component = renderWithRouter(); + expect(component).toMatchSnapshot(); + }); + + it('renders expected title for valid overview route', () => { + const component = renderWithRouter( + + + + ); + expect(component).toMatchSnapshot(); + + const titleComponent = component.find('.euiTitle'); + expect(titleComponent.text()).toBe('Overview'); + }); + + it('renders expected title for valid monitor route', () => { + const history = createMemoryHistory({ initialEntries: ['/monitor/ZWxhc3RpYy1jbw=='] }); + + const component = renderWithRouter( + + + , + history + ); + expect(component).toMatchSnapshot(); + + const titleComponent = component.find('.euiTitle'); + expect(titleComponent.text()).toBe('https://www.elastic.co'); + }); + + it('mount expected page title for valid monitor route', () => { + const history = createMemoryHistory({ initialEntries: ['/monitor/ZWxhc3RpYy1jbw=='] }); + + const component = mountWithRouter( + + + , + history + ); + expect(component).toMatchSnapshot(); + + const titleComponent = component.find('.euiTitle'); + expect(titleComponent.text()).toBe('https://www.elastic.co'); + expect(document.title).toBe('Uptime | elastic - Kibana'); + }); + + it('mount and set expected breadcrumb for monitor route', () => { + const history = createMemoryHistory({ initialEntries: ['/monitor/ZWxhc3RpYy1jbw=='] }); + let breadcrumbObj: ChromeBreadcrumb[] = []; + const setBreadcrumb = (breadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = breadcrumbs; + }; + + mountWithRouter( + + + , + history + ); + + expect(breadcrumbObj).toStrictEqual([ + { href: '#/?', text: 'Uptime' }, + { text: 'https://www.elastic.co' }, + ]); + }); + + it('mount and set expected breadcrumb for overview route', () => { + let breadcrumbObj: ChromeBreadcrumb[] = []; + const setBreadcrumb = (breadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = breadcrumbs; + }; + + mountWithRouter( + + + + ); + + expect(breadcrumbObj).toStrictEqual([{ href: '#/', text: 'Uptime' }]); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/index.ts b/x-pack/legacy/plugins/uptime/public/pages/index.ts index 17f083ca023ed..3f74bda79bd46 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/index.ts +++ b/x-pack/legacy/plugins/uptime/public/pages/index.ts @@ -6,5 +6,3 @@ export { MonitorPage } from './monitor'; export { NotFoundPage } from './not_found'; -export { PageHeader } from './page_header'; -export { OverviewPage } from '../components/connected/'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index a8501ff14313a..1d45c7b7ab99b 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -5,21 +5,15 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { Fragment, useContext, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { useParams } from 'react-router-dom'; import { MonitorCharts, PingList } from '../components/functional'; -import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeRefreshContext, UptimeThemeContext } from '../contexts'; import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { PageHeader } from './page_header'; import { MonitorStatusDetails } from '../components/connected'; -interface MonitorPageProps { - setBreadcrumbs: UMUpdateBreadcrumbs; -} - -export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { +export const MonitorPage = () => { // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url let { monitorId } = useParams(); monitorId = atob(monitorId || ''); @@ -46,8 +40,7 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { useTrackPageview({ app: 'uptime', path: 'monitor', delay: 15000 }); return ( - - + <> @@ -69,6 +62,6 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { status: selectedPingStatus, }} /> - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ce5fb619aca02..ae7457e835c94 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -13,11 +13,9 @@ import { OverviewPageParsingErrorCallout, StatusPanel, } from '../components/functional'; -import { UMUpdateBreadcrumbs } from '../lib/lib'; import { useUrlParams, useUptimeTelemetry, UptimePage } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; -import { PageHeader } from './page_header'; import { DataPublicPluginStart, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts'; import { FilterGroup, KueryBar } from '../components/connected'; @@ -25,7 +23,6 @@ import { useUpdateKueryString } from '../hooks'; interface OverviewPageProps { autocomplete: DataPublicPluginStart['autocomplete']; - setBreadcrumbs: UMUpdateBreadcrumbs; indexPattern: IIndexPattern; setEsKueryFilters: (esFilters: string) => void; } @@ -41,12 +38,7 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -export const OverviewPageComponent = ({ - autocomplete, - setBreadcrumbs, - indexPattern, - setEsKueryFilters, -}: Props) => { +export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFilters }: Props) => { const { colors } = useContext(UptimeThemeContext); const [getUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); @@ -81,7 +73,6 @@ export const OverviewPageComponent = ({ return ( <> - diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx index 7c3f80d4beb98..5c051c491c6f5 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx @@ -4,23 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { UptimeDatePicker } from '../components/functional/uptime_date_picker'; -import { AppState } from '../state'; -import { selectSelectedMonitor } from '../state/selectors'; import { getMonitorPageBreadcrumb, getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { getTitle } from '../lib/helper/get_title'; import { UMUpdateBreadcrumbs } from '../lib/lib'; -import { MONITOR_ROUTE } from '../routes'; import { useUrlParams } from '../hooks'; +import { MONITOR_ROUTE } from '../../common/constants'; +import { Ping } from '../../common/graphql/types'; interface PageHeaderProps { - monitorStatus?: any; + monitorStatus?: Ping; setBreadcrumbs: UMUpdateBreadcrumbs; } @@ -32,24 +30,27 @@ export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeade const [getUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); - const headingText = i18n.translate('xpack.uptime.overviewPage.headerText', { - defaultMessage: 'Overview', - description: `The text that will be displayed in the app's heading when the Overview page loads.`, - }); + const headingText = !monitorPage + ? i18n.translate('xpack.uptime.overviewPage.headerText', { + defaultMessage: 'Overview', + description: `The text that will be displayed in the app's heading when the Overview page loads.`, + }) + : monitorStatus?.url?.full; const [headerText, setHeaderText] = useState(headingText); useEffect(() => { if (monitorPage) { - setHeaderText(monitorStatus?.url?.full); + setHeaderText(monitorStatus?.url?.full ?? ''); if (monitorStatus?.monitor) { const { name, id } = monitorStatus.monitor; - document.title = getTitle(name || id); + document.title = getTitle((name || id) ?? ''); } } else { + setHeaderText(headingText); document.title = getTitle(); } - }, [monitorStatus, monitorPage, setHeaderText]); + }, [monitorStatus, monitorPage, setHeaderText, headingText]); useEffect(() => { if (monitorPage) { @@ -61,10 +62,6 @@ export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeade } }, [headerText, setBreadcrumbs, params, monitorPage]); - useEffect(() => { - document.title = getTitle(); - }, []); - return ( <> @@ -81,9 +78,3 @@ export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeade ); }; - -const mapStateToProps = (state: AppState) => ({ - monitorStatus: selectSelectedMonitor(state), -}); - -export const PageHeader = connect(mapStateToProps, null)(PageHeaderComponent); diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index c318a82ab7f19..0f726d89e0d28 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -6,26 +6,22 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { MonitorPage, NotFoundPage, OverviewPage } from './pages'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { UMUpdateBreadcrumbs } from './lib/lib'; - -export const MONITOR_ROUTE = '/monitor/:monitorId?'; -export const OVERVIEW_ROUTE = '/'; +import { OverviewPage } from './components/connected/pages/overview_container'; +import { MONITOR_ROUTE, OVERVIEW_ROUTE } from '../common/constants'; +import { MonitorPage, NotFoundPage } from './pages'; interface RouterProps { autocomplete: DataPublicPluginStart['autocomplete']; - basePath: string; - setBreadcrumbs: UMUpdateBreadcrumbs; } -export const PageRouter: FC = ({ autocomplete, basePath, setBreadcrumbs }) => ( +export const PageRouter: FC = ({ autocomplete }) => ( - + - + diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index baaed6616b653..dbde9f8b6a8c0 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -23,6 +23,7 @@ import { CommonlyUsedRange } from './components/functional/uptime_date_picker'; import { store } from './state'; import { setBasePath } from './state/actions'; import { PageRouter } from './routes'; +import { PageHeader } from './components/connected/pages/page_header_container'; export interface UptimeAppColors { danger: string; @@ -98,12 +99,9 @@ const Application = (props: UptimeAppProps) => {
- + + // @ts-ignore we need to update the type of this prop +
diff --git a/x-pack/plugins/canvas/server/collectors/collector_helpers.ts b/x-pack/plugins/canvas/server/collectors/collector_helpers.ts index 784042fb4d94d..73de691dae05f 100644 --- a/x-pack/plugins/canvas/server/collectors/collector_helpers.ts +++ b/x-pack/plugins/canvas/server/collectors/collector_helpers.ts @@ -9,25 +9,30 @@ * @param cb: callback to do something with a function that has been found */ -import { ExpressionAST, ExpressionArgAST } from '../../types'; +import { + ExpressionAstExpression, + ExpressionAstNode, +} from '../../../../../src/plugins/expressions/common'; -function isExpression(maybeExpression: ExpressionArgAST): maybeExpression is ExpressionAST { - return typeof maybeExpression === 'object'; +function isExpression( + maybeExpression: ExpressionAstNode +): maybeExpression is ExpressionAstExpression { + return typeof maybeExpression === 'object' && maybeExpression.type === 'expression'; } -export function collectFns(ast: ExpressionArgAST, cb: (functionName: string) => void) { - if (isExpression(ast)) { - ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => { - cb(cFunction); +export function collectFns(ast: ExpressionAstNode, cb: (functionName: string) => void) { + if (!isExpression(ast)) return; - // recurse the arguments and update the set along the way - Object.keys(cArguments).forEach(argName => { - cArguments[argName].forEach(subAst => { - if (subAst != null) { - collectFns(subAst, cb); - } - }); + ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => { + cb(cFunction); + + // recurse the arguments and update the set along the way + Object.keys(cArguments).forEach(argName => { + cArguments[argName].forEach(subAst => { + if (subAst != null) { + collectFns(subAst, cb); + } }); }); - } + }); } diff --git a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts index 5f1944bea3eaa..ae71600d24e4b 100644 --- a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts @@ -6,14 +6,13 @@ import { SearchParams } from 'elasticsearch'; import { get } from 'lodash'; -import { fromExpression } from '@kbn/interpreter/common'; import { collectFns } from './collector_helpers'; import { - ExpressionAST, TelemetryCollector, TelemetryCustomElement, TelemetryCustomElementDocument, } from '../../types'; +import { parseExpression } from '../../../../../src/plugins/expressions/common'; const CUSTOM_ELEMENT_TYPE = 'canvas-element'; interface CustomElementSearch { @@ -79,7 +78,7 @@ export function summarizeCustomElements( parsedContents.map(contents => { contents.selectedNodes.map(node => { - const ast: ExpressionAST = fromExpression(node.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed + const ast = parseExpression(node.expression); collectFns(ast, (cFunction: string) => { functionSet.add(cFunction); }); diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 6c86b8b2c7468..9c088958c748f 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -6,10 +6,10 @@ import { SearchParams } from 'elasticsearch'; import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; -import { fromExpression } from '@kbn/interpreter/common'; import { CANVAS_TYPE } from '../../../../legacy/plugins/canvas/common/lib/constants'; import { collectFns } from './collector_helpers'; -import { ExpressionAST, TelemetryCollector, CanvasWorkpad } from '../../types'; +import { TelemetryCollector, CanvasWorkpad } from '../../types'; +import { parseExpression } from '../../../../../src/plugins/expressions/common'; interface WorkpadSearch { [CANVAS_TYPE]: CanvasWorkpad; @@ -73,7 +73,7 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr ); const functionCounts = workpad.pages.reduce((accum, page) => { return page.elements.map(element => { - const ast: ExpressionAST = fromExpression(element.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed + const ast = parseExpression(element.expression); collectFns(ast, cFunction => { functionSet.add(cFunction); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e9a5d9611c806..dbc6a015f9c97 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1286,8 +1286,8 @@ "kbn.home.welcomeDescription": "Elastic Stack への開かれた窓", "kbn.home.welcomeHomePageHeader": "Kibana ホーム", "kbn.home.welcomeTitle": "Kibana へようこそ", - "kbn.management.advancedSettings.badge.readOnly.text": "読み込み専用", - "kbn.management.advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", + "advancedSettings.badge.readOnly.text": "読み込み専用", + "advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", "kbn.management.createIndexPattern.betaLabel": "ベータ", "kbn.management.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "kbn.management.createIndexPattern.emptyStateHeader": "Elasticsearch データが見つかりませんでした", @@ -1575,53 +1575,50 @@ "kbn.management.objects.view.viewItemTitle": "{title} を表示", "kbn.management.savedObjects.editBreadcrumb": "{savedObjectType} を編集", "kbn.management.savedObjects.indexBreadcrumb": "保存されたオブジェクト", - "advancedSettings.advancedSettingsDescription": "Kibana の動作を管理する設定を直接変更します。", "advancedSettings.advancedSettingsLabel": "高度な設定", - "kbn.management.settings.breadcrumb": "高度な設定", - "kbn.management.settings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", - "kbn.management.settings.callOutCautionTitle": "注意:不具合が起こる可能性があります", - "kbn.management.settings.categoryNames.dashboardLabel": "ダッシュボード", - "kbn.management.settings.categoryNames.discoverLabel": "ディスカバリ", - "kbn.management.settings.categoryNames.generalLabel": "一般", - "kbn.management.settings.categoryNames.notificationsLabel": "通知", - "kbn.management.settings.categoryNames.reportingLabel": "レポート", - "kbn.management.settings.categoryNames.searchLabel": "検索", - "kbn.management.settings.categoryNames.siemLabel": "SIEM", - "kbn.management.settings.categoryNames.timelionLabel": "Timelion", - "kbn.management.settings.categoryNames.visualizationsLabel": "ビジュアライゼーション", - "kbn.management.settings.categorySearchLabel": "カテゴリー", - "kbn.management.settings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", - "kbn.management.settings.field.cancelEditingButtonLabel": "キャンセル", - "kbn.management.settings.field.changeImageLinkAriaLabel": "{ariaName} を変更", - "kbn.management.settings.field.changeImageLinkText": "画像を変更", - "kbn.management.settings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", - "kbn.management.settings.field.customSettingAriaLabel": "カスタム設定", - "kbn.management.settings.field.customSettingTooltip": "カスタム設定", - "kbn.management.settings.field.defaultValueText": "デフォルト: {value}", - "kbn.management.settings.field.defaultValueTypeJsonText": "デフォルト: {value}", - "kbn.management.settings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", - "kbn.management.settings.field.imageChangeErrorMessage": "画像を保存できませんでした", - "kbn.management.settings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", - "kbn.management.settings.field.offLabel": "オフ", - "kbn.management.settings.field.onLabel": "オン", - "kbn.management.settings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "kbn.management.settings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", - "kbn.management.settings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", - "kbn.management.settings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", - "kbn.management.settings.field.resetToDefaultLinkText": "デフォルトにリセット", - "kbn.management.settings.field.saveButtonAriaLabel": "{ariaName} を保存", - "kbn.management.settings.field.saveButtonLabel": "保存", - "kbn.management.settings.field.saveFieldErrorMessage": "{name} を保存できませんでした", - "kbn.management.settings.form.clearNoSearchResultText": "(検索結果を消去)", - "kbn.management.settings.form.clearSearchResultText": "(検索結果を消去)", - "kbn.management.settings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", - "kbn.management.settings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", + "advancedSettings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", + "advancedSettings.callOutCautionTitle": "注意:不具合が起こる可能性があります", + "advancedSettings.categoryNames.dashboardLabel": "ダッシュボード", + "advancedSettings.categoryNames.discoverLabel": "ディスカバリ", + "advancedSettings.categoryNames.generalLabel": "一般", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "レポート", + "advancedSettings.categoryNames.searchLabel": "検索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", + "advancedSettings.categorySearchLabel": "カテゴリー", + "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", + "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", + "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", + "advancedSettings.field.changeImageLinkText": "画像を変更", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", + "advancedSettings.field.customSettingAriaLabel": "カスタム設定", + "advancedSettings.field.customSettingTooltip": "カスタム設定", + "advancedSettings.field.defaultValueText": "デフォルト: {value}", + "advancedSettings.field.defaultValueTypeJsonText": "デフォルト: {value}", + "advancedSettings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", + "advancedSettings.field.imageChangeErrorMessage": "画像を保存できませんでした", + "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", + "advancedSettings.field.offLabel": "オフ", + "advancedSettings.field.onLabel": "オン", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", + "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", + "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", + "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", + "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", + "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", + "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", + "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", + "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", "advancedSettings.pageTitle": "設定", - "kbn.management.settings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", - "kbn.management.settings.searchBarAriaLabel": "高度な設定を検索", - "kbn.management.settings.sectionLabel": "高度な設定", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", + "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "kbn.managementTitle": "管理", - "kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", "kbn.topNavMenu.openInspectorButtonLabel": "検査", "kbn.topNavMenu.refreshButtonLabel": "更新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 201e3c35ee282..4a2c33eba79da 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1286,8 +1286,8 @@ "kbn.home.welcomeDescription": "您了解 Elastic Stack 的窗口", "kbn.home.welcomeHomePageHeader": "Kibana 主页", "kbn.home.welcomeTitle": "欢迎使用 Kibana", - "kbn.management.advancedSettings.badge.readOnly.text": "只读", - "kbn.management.advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", + "advancedSettings.badge.readOnly.text": "只读", + "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", "kbn.management.createIndexPattern.betaLabel": "公测版", "kbn.management.createIndexPattern.emptyState.checkDataButton": "检查新数据", "kbn.management.createIndexPattern.emptyStateHeader": "找不到任何 Elasticsearch 数据", @@ -1575,53 +1575,50 @@ "kbn.management.objects.view.viewItemTitle": "查看“{title}”", "kbn.management.savedObjects.editBreadcrumb": "编辑 {savedObjectType}", "kbn.management.savedObjects.indexBreadcrumb": "已保存对象", - "advancedSettings.advancedSettingsDescription": "直接编辑在 Kibana 中控制行为的设置。", "advancedSettings.advancedSettingsLabel": "高级设置", - "kbn.management.settings.breadcrumb": "高级设置", - "kbn.management.settings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", - "kbn.management.settings.callOutCautionTitle": "注意:在这里您可能会使问题出现", - "kbn.management.settings.categoryNames.dashboardLabel": "仪表板", - "kbn.management.settings.categoryNames.discoverLabel": "Discover", - "kbn.management.settings.categoryNames.generalLabel": "常规", - "kbn.management.settings.categoryNames.notificationsLabel": "通知", - "kbn.management.settings.categoryNames.reportingLabel": "报告", - "kbn.management.settings.categoryNames.searchLabel": "搜索", - "kbn.management.settings.categoryNames.siemLabel": "SIEM", - "kbn.management.settings.categoryNames.timelionLabel": "Timelion", - "kbn.management.settings.categoryNames.visualizationsLabel": "可视化", - "kbn.management.settings.categorySearchLabel": "类别", - "kbn.management.settings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", - "kbn.management.settings.field.cancelEditingButtonLabel": "取消", - "kbn.management.settings.field.changeImageLinkAriaLabel": "更改 {ariaName}", - "kbn.management.settings.field.changeImageLinkText": "更改图片", - "kbn.management.settings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", - "kbn.management.settings.field.customSettingAriaLabel": "定制设置", - "kbn.management.settings.field.customSettingTooltip": "定制设置", - "kbn.management.settings.field.defaultValueText": "默认值:{value}", - "kbn.management.settings.field.defaultValueTypeJsonText": "默认值:{value}", - "kbn.management.settings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", - "kbn.management.settings.field.imageChangeErrorMessage": "图片无法保存", - "kbn.management.settings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", - "kbn.management.settings.field.offLabel": "关闭", - "kbn.management.settings.field.onLabel": "开启", - "kbn.management.settings.field.requiresPageReloadToastButtonLabel": "重新加载页面", - "kbn.management.settings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", - "kbn.management.settings.field.resetFieldErrorMessage": "无法重置 {name}", - "kbn.management.settings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", - "kbn.management.settings.field.resetToDefaultLinkText": "重置为默认值", - "kbn.management.settings.field.saveButtonAriaLabel": "保存 {ariaName}", - "kbn.management.settings.field.saveButtonLabel": "保存", - "kbn.management.settings.field.saveFieldErrorMessage": "无法保存 {name}", - "kbn.management.settings.form.clearNoSearchResultText": "(清除搜索)", - "kbn.management.settings.form.clearSearchResultText": "(清除搜索)", - "kbn.management.settings.form.noSearchResultText": "未找到设置{clearSearch}", - "kbn.management.settings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", + "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", + "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", + "advancedSettings.categoryNames.dashboardLabel": "仪表板", + "advancedSettings.categoryNames.discoverLabel": "Discover", + "advancedSettings.categoryNames.generalLabel": "常规", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "报告", + "advancedSettings.categoryNames.searchLabel": "搜索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "可视化", + "advancedSettings.categorySearchLabel": "类别", + "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", + "advancedSettings.field.cancelEditingButtonLabel": "取消", + "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", + "advancedSettings.field.changeImageLinkText": "更改图片", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", + "advancedSettings.field.customSettingAriaLabel": "定制设置", + "advancedSettings.field.customSettingTooltip": "定制设置", + "advancedSettings.field.defaultValueText": "默认值:{value}", + "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", + "advancedSettings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", + "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", + "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", + "advancedSettings.field.offLabel": "关闭", + "advancedSettings.field.onLabel": "开启", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", + "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", + "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", + "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", + "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", + "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", + "advancedSettings.form.clearSearchResultText": "(清除搜索)", + "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", + "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", "advancedSettings.pageTitle": "设置", - "kbn.management.settings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", - "kbn.management.settings.searchBarAriaLabel": "搜索高级设置", - "kbn.management.settings.sectionLabel": "高级设置", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", + "advancedSettings.searchBarAriaLabel": "搜索高级设置", "kbn.managementTitle": "管理", - "kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", "kbn.topNavMenu.openInspectorButtonLabel": "检查", "kbn.topNavMenu.refreshButtonLabel": "刷新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx index ecb284352b98c..27aa3ba93684e 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { of } from 'rxjs'; import { ComponentType } from 'enzyme'; import { - chromeServiceMock, docLinksServiceMock, uiSettingsServiceMock, notificationServiceMock, @@ -31,8 +30,7 @@ class MockTimeBuckets { export const mockContextValue = { licenseStatus$: of({ valid: true }), docLinks: docLinksServiceMock.createStartContract(), - chrome: chromeServiceMock.createStartContract(), - MANAGEMENT_BREADCRUMB: { text: 'test' }, + setBreadcrumbs: jest.fn(), createTimeBuckets: () => new MockTimeBuckets(), uiSettings: uiSettingsServiceMock.createSetupContract(), toasts: notificationServiceMock.createSetupContract().toasts, diff --git a/x-pack/plugins/watcher/public/application/app.tsx b/x-pack/plugins/watcher/public/application/app.tsx index 7ca79bb558baa..f4b9441719386 100644 --- a/x-pack/plugins/watcher/public/application/app.tsx +++ b/x-pack/plugins/watcher/public/application/app.tsx @@ -6,13 +6,7 @@ import React, { useEffect, useState } from 'react'; import { Observable } from 'rxjs'; -import { - ChromeStart, - DocLinksStart, - HttpSetup, - ToastsSetup, - IUiSettingsClient, -} from 'kibana/public'; +import { DocLinksStart, HttpSetup, ToastsSetup, IUiSettingsClient } from 'kibana/public'; import { HashRouter, @@ -27,6 +21,8 @@ import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public'; + import { LicenseStatus } from '../../common/types/license_status'; import { WatchStatus } from './sections/watch_status/components/watch_status'; import { WatchEdit } from './sections/watch_edit/components/watch_edit'; @@ -42,7 +38,6 @@ const ShareRouter = withRouter(({ children, history }: RouteComponentProps & { c }); export interface AppDeps { - chrome: ChromeStart; docLinks: DocLinksStart; toasts: ToastsSetup; http: HttpSetup; @@ -50,7 +45,7 @@ export interface AppDeps { theme: ChartsPluginSetup['theme']; createTimeBuckets: () => any; licenseStatus$: Observable; - MANAGEMENT_BREADCRUMB: any; + setBreadcrumbs: Parameters[0]['setBreadcrumbs']; } export const App = (deps: AppDeps) => { diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/watch_edit.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/watch_edit.tsx index 59a6079d74b42..f125dde63f78d 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/watch_edit.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/watch_edit.tsx @@ -96,7 +96,7 @@ export const WatchEdit = ({ }; }) => { // hooks - const { MANAGEMENT_BREADCRUMB, chrome } = useAppContext(); + const { setBreadcrumbs } = useAppContext(); const [{ watch, loadError }, dispatch] = useReducer(watchReducer, { watch: null }); const setWatchProperty = (property: string, value: any) => { @@ -128,12 +128,8 @@ export const WatchEdit = ({ }, [id, type]); useEffect(() => { - chrome.setBreadcrumbs([ - MANAGEMENT_BREADCRUMB, - listBreadcrumb, - id ? editBreadcrumb : createBreadcrumb, - ]); - }, [id, chrome, MANAGEMENT_BREADCRUMB]); + setBreadcrumbs([listBreadcrumb, id ? editBreadcrumb : createBreadcrumb]); + }, [id, setBreadcrumbs]); const errorCode = getPageErrorCode(loadError); if (errorCode) { diff --git a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx index 9f6a8ddbc843e..2d552d7fbdda6 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx @@ -46,8 +46,7 @@ import { useAppContext } from '../../../app_context'; export const WatchList = () => { // hooks const { - chrome, - MANAGEMENT_BREADCRUMB, + setBreadcrumbs, links: { watcherGettingStartedUrl }, } = useAppContext(); const [selection, setSelection] = useState([]); @@ -57,8 +56,8 @@ export const WatchList = () => { const [deletedWatches, setDeletedWatches] = useState([]); useEffect(() => { - chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb]); - }, [chrome, MANAGEMENT_BREADCRUMB]); + setBreadcrumbs([listBreadcrumb]); + }, [setBreadcrumbs]); const { isLoading: isWatchesLoading, data: watches, error } = useLoadWatches( REFRESH_INTERVALS.WATCH_LIST diff --git a/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_status.tsx b/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_status.tsx index b15c047d06f67..5198b0e45c6dc 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_status.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_status.tsx @@ -67,7 +67,7 @@ export const WatchStatus = ({ }; }; }) => { - const { chrome, MANAGEMENT_BREADCRUMB, toasts } = useAppContext(); + const { setBreadcrumbs, toasts } = useAppContext(); const { error: watchDetailError, data: watchDetail, @@ -80,8 +80,8 @@ export const WatchStatus = ({ const [isTogglingActivation, setIsTogglingActivation] = useState(false); useEffect(() => { - chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]); - }, [id, chrome, MANAGEMENT_BREADCRUMB]); + setBreadcrumbs([listBreadcrumb, statusBreadcrumb]); + }, [id, setBreadcrumbs]); const errorCode = getPageErrorCode(watchDetailError); diff --git a/x-pack/plugins/watcher/public/legacy/index.d.ts b/x-pack/plugins/watcher/public/legacy/index.d.ts deleted file mode 100644 index 307e365040fb7..0000000000000 --- a/x-pack/plugins/watcher/public/legacy/index.d.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 declare const MANAGEMENT_BREADCRUMB: { text: string; href?: string }; diff --git a/x-pack/plugins/watcher/public/legacy/index.ts b/x-pack/plugins/watcher/public/legacy/index.ts index d14081a667acc..cdb656fc0cda8 100644 --- a/x-pack/plugins/watcher/public/legacy/index.ts +++ b/x-pack/plugins/watcher/public/legacy/index.ts @@ -3,13 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - export { TimeBuckets } from './time_buckets'; - -export const MANAGEMENT_BREADCRUMB = Object.freeze({ - text: i18n.translate('xpack.watcher.management.breadcrumb', { - defaultMessage: 'Management', - }), - href: '#/management', -}); diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 354edd2078676..cb9ad4eb21fcf 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -12,7 +12,7 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LicenseStatus } from '../common/types/license_status'; import { ILicense, LICENSE_CHECK_STATE } from '../../licensing/public'; -import { TimeBuckets, MANAGEMENT_BREADCRUMB } from './legacy'; +import { TimeBuckets } from './legacy'; import { PLUGIN } from '../common/constants'; import { Dependencies } from './types'; @@ -37,9 +37,9 @@ export class WatcherUIPlugin implements Plugin { 'xpack.watcher.sections.watchList.managementSection.watcherDisplayName', { defaultMessage: 'Watcher' } ), - mount: async ({ element }) => { + mount: async ({ element, setBreadcrumbs }) => { const [core] = await getStartServices(); - const { chrome, i18n: i18nDep, docLinks, savedObjects } = core; + const { i18n: i18nDep, docLinks, savedObjects } = core; const { boot } = await import('./application/boot'); return boot({ @@ -51,12 +51,11 @@ export class WatcherUIPlugin implements Plugin { http, uiSettings, docLinks, - chrome, + setBreadcrumbs, theme: charts.theme, savedObjects: savedObjects.client, I18nContext: i18nDep.Context, createTimeBuckets: () => new TimeBuckets(uiSettings, data), - MANAGEMENT_BREADCRUMB, }); }, }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 2649c5d26309d..6efaae70e089b 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -178,12 +178,12 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Discover', 'Stack Management']); }); - it(`does not allow navigation to advanced settings; redirects to Kibana home`, async () => { + it(`does not allow navigation to advanced settings; redirects to management home`, async () => { await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('homeApp', { + await testSubjects.existOrFail('managementHome', { timeout: 10000, }); }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 79bb10e0bded1..c780a8efae304 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -74,13 +74,13 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('empty_kibana'); }); - it(`redirects to Kibana home`, async () => { + it(`redirects to management home`, async () => { await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { basePath: `/s/custom_space`, ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('homeApp', { + await testSubjects.existOrFail('managementHome', { timeout: 10000, }); }); diff --git a/yarn.lock b/yarn.lock index 51edd382e6394..09a28ac130de7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18858,9 +18858,9 @@ kind-of@^5.0.0, kind-of@^5.0.2: integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klaw@^1.0.0: version "1.3.1"