diff --git a/.backportrc.json b/.backportrc.json index 3f1d639e9a480..a97c82ca0efa9 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.10", "7.9", "7.8", "7.7", @@ -27,7 +28,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.10.0$": "7.x", + "^v7.11.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" } } diff --git a/.eslintignore b/.eslintignore index 93c69b4f9b207..7f3e3ef597cbb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,19 +19,14 @@ target # plugin overrides /src/core/lib/kbn_internal_native_observable /src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken -/src/legacy/ui/public/flot-charts /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/public/_generated_/** -/src/plugins/vis_type_timelion/public/flot/jquery.flot.* -/src/plugins/timelion/public/flot/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/**/snapshots.js /x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin -/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build -/x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -48,4 +43,4 @@ target /packages/kbn-ui-framework/dist /packages/kbn-ui-framework/doc_site/build /packages/kbn-ui-framework/generator-kui/*/templates/ - +/packages/kbn-ui-shared-deps/flot_charts diff --git a/.eslintrc.js b/.eslintrc.js index 27dacd51be6f2..24ae50791d91d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1178,13 +1178,7 @@ module.exports = { }, }, { - files: ['x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/**/*.js'], - env: { - jquery: true, - }, - }, - { - files: ['x-pack/plugins/monitoring/public/lib/jquery_flot/**/*.js'], + files: ['packages/kbn-ui-shared-deps/flot_charts/**/*.js'], env: { jquery: true, }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5dd41581914ed..8f2c27ac7c3cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,20 +6,16 @@ # used for the 'team' designator within Kibana Stats # App -/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app -/src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app -/src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/management/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app /src/plugins/vis_default_editor/ @elastic/kibana-app -/src/plugins/vis_type_markdown/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app /src/plugins/vis_type_table/ @elastic/kibana-app /src/plugins/vis_type_tagcloud/ @elastic/kibana-app @@ -35,10 +31,8 @@ #CC# /src/legacy/core_plugins/kibana/common/utils @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/migrations @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app #CC# /src/legacy/core_plugins/timelion @elastic/kibana-app #CC# /src/legacy/core_plugins/vis_type_tagcloud @elastic/kibana-app #CC# /src/legacy/core_plugins/vis_type_vega @elastic/kibana-app @@ -46,8 +40,6 @@ #CC# /src/legacy/server/url_shortening/ @elastic/kibana-app #CC# /src/legacy/ui/public/state_management @elastic/kibana-app #CC# /src/plugins/index_pattern_management/public @elastic/kibana-app -#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app -#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -127,10 +119,18 @@ #CC# /x-pack/plugins/beats_management/ @elastic/beats # Canvas +/src/plugins/dashboard/ @elastic/kibana-app +/src/plugins/input_control_vis/ @elastic/kibana-app +/src/plugins/vis_type_markdown/ @elastic/kibana-app /x-pack/plugins/canvas/ @elastic/kibana-canvas +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas #CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md new file mode 100644 index 0000000000000..86d2b1405d4eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -0,0 +1,37 @@ +--- +name: Security Solution Bug Report +about: Things break. Help us identify those things so we can fix them! +title: '[Security Solution]' +--- + +**Describe the bug:** + +**Kibana/Elasticsearch Stack version:** + +**Server OS version:** + +**Browser and Browser OS versions:** + +**Elastic Endpoint version:** + +**Original install method (e.g. download page, yum, from source, etc.):** + +**Functional Area (e.g. Endpoint management, timelines, resolver, etc.):** + +**Steps to reproduce:** + +1. +2. +3. + +**Current behavior:** + +**Expected behavior:** + +**Screenshots (if relevant):** + +**Errors in browser console (if relevant):** + +**Provide logs and/or server output (if relevant):** + +**Any additional context (logs, chat logs, magical formulas, etc.):** diff --git a/.i18nrc.json b/.i18nrc.json index e0281b0a5bc21..68e38d3976a68 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -11,6 +11,7 @@ "uiActionsExamples": "examples/ui_action_examples", "share": "src/plugins/share", "home": "src/plugins/home", + "flot": "packages/kbn-ui-shared-deps/flot_charts", "charts": "src/plugins/charts", "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index 4887eb6ca870d..f49e2a944c900 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -10,7 +10,7 @@ When you've finished your workpad, you can share it outside of {kib}. Create a JSON file of your workpad that you can export outside of {kib}. -Click *Share > Download as JSON*. +To begin, click *Share > Download as JSON*. [role="screenshot"] image::images/canvas-export-workpad.png[Export single workpad through JSON, from Share dropdown] @@ -23,7 +23,7 @@ Want to export multiple workpads? Go to the *Canvas* home page, select the workp If you have a subscription that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. -Click *Share > PDF reports > Generate PDF*. +To begin, click *Share > PDF reports > Generate PDF*. [role="screenshot"] image::images/canvas-generate-pdf.gif[Image showing how to generate a PDF] @@ -36,7 +36,7 @@ For more information, refer to <> or a script. -Click *Share > PDF reports > Copy POST URL*. +To begin, click *Share > PDF reports > Copy POST URL*. [role="screenshot"] image::images/canvas-create-URL.gif[Image showing how to create POST URL] diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index ea4d2c8cc6a83..312391541a777 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -2,7 +2,7 @@ [[canvas-tutorial]] == Tutorial: Create a workpad for monitoring sales -To get up and running with Canvas, use the following tutorial where you'll create a workpad for monitoring sales at an eCommerce store. +To get up and running with Canvas, add the Sample eCommerce orders data, then use the data to create a workpad for monitoring sales at an eCommerce store. [float] === Before you begin @@ -114,18 +114,16 @@ image::images/canvas-timefilter-element.png[Image showing Canvas workpad with fi To see how the data changes, set the time filter to *Last 7 days*. As you change the time filter options, the elements automatically update. -Your workpad is now complete! +Your workpad is complete! [float] -=== Next steps +=== What's next? Now that you know the Canvas basics, you're ready to explore on your own. Here are some things to try: * Play with the {kibana-ref}/add-sample-data.html[sample Canvas workpads]. -* Build presentations of your own live data with <>. - -* Learn more about <> — the building blocks of your workpad. +* Build presentations of your own data with <>. * Deep dive into the {kibana-ref}/canvas-function-reference.html[expression language and functions] that drive Canvas. diff --git a/docs/canvas/images/canvas-autoplay-interval.png b/docs/canvas/images/canvas-autoplay-interval.png index 68a7ca248d9ee..a7b1251efc808 100644 Binary files a/docs/canvas/images/canvas-autoplay-interval.png and b/docs/canvas/images/canvas-autoplay-interval.png differ diff --git a/docs/canvas/images/canvas-gs-example.png b/docs/canvas/images/canvas-gs-example.png index 90beccd322aa4..a9b960342709f 100644 Binary files a/docs/canvas/images/canvas-gs-example.png and b/docs/canvas/images/canvas-gs-example.png differ diff --git a/docs/canvas/images/canvas-refresh-interval.png b/docs/canvas/images/canvas-refresh-interval.png index 62e88ad4bf7d0..c097d950a7ec7 100644 Binary files a/docs/canvas/images/canvas-refresh-interval.png and b/docs/canvas/images/canvas-refresh-interval.png differ diff --git a/docs/canvas/images/canvas-zoom-controls.png b/docs/canvas/images/canvas-zoom-controls.png index 5c72d118041e4..1407ca3cd8627 100644 Binary files a/docs/canvas/images/canvas-zoom-controls.png and b/docs/canvas/images/canvas-zoom-controls.png differ diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 3321aae3c0994..a2cda1e0b1e87 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -19,7 +19,7 @@ More details are available in the https://www.typescriptlang.org/docs/handbook/p ==== Caveats This architecture imposes several limitations to which we must comply: -- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. +- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. https://github.com/elastic/kibana/issues/78162 is going to provide a tool to find such problem places. - A project must emit its type declaration. It's not always possible to generate a type declaration if the compiler cannot infer a type. There are two basic cases: 1. Your plugin exports a type inferring an internal type declared in Kibana codebase. In this case, you'll have to either export an internal type or to declare an exported type explicitly. @@ -27,7 +27,8 @@ This architecture imposes several limitations to which we must comply: [discrete] ==== Prerequisites -Since `tsc` doesn't support circular project references, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. +Since project refs rely on generated `d.ts` files, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. +https://github.com/elastic/kibana/issues/79343 is going to provide a tool for identifying a plugin dependency tree. [discrete] ==== Implementation diff --git a/docs/developer/contributing/development-documentation.asciidoc b/docs/developer/contributing/development-documentation.asciidoc index 99e55963f57af..70dd756ca808e 100644 --- a/docs/developer/contributing/development-documentation.asciidoc +++ b/docs/developer/contributing/development-documentation.asciidoc @@ -26,6 +26,13 @@ README for getting the docs tooling set up. ```bash node scripts/docs.js --open ``` +[discrete] +==== REST APIs + +REST APIs should be documented using the following recommended formats: + +* https://raw.githubusercontent.com/elastic/docs/master/shared/api-ref-ex.asciidoc[API doc templaate] +* https://raw.githubusercontent.com/elastic/docs/master/shared/api-definitions-ex.asciidoc[API object definition template] [discrete] === General developer documentation and guidelines diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index ecb37ffe9c97b..ba4ab89d17c27 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -49,7 +49,7 @@ The Release Notes summarize what the PRs accomplish in language that is meaningf The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description. -To use a single paragraph of text, enter `Release note:` or a `## Release note` header in the PR description, followed by your text. For example, refer to this https://github.com/elastic/kibana/pull/65796[PR] that uses the `## Release note` header. +To use a single paragraph of text, enter a `Release note:` or `## Release note` header in the PR description ("dev docs" works too), followed by your text. For example, refer to this https://github.com/elastic/kibana/pull/65796[PR] that uses the `## Release note` header. When you create the Release Notes text, use the following best practices: diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 21c51f8cabd32..b5a810852b94d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -435,8 +435,9 @@ using the CURL scripts in the scripts folder. |This plugin provides access to the detailed tile map services from Elastic. -|{kib-repo}blob/{branch}/x-pack/plugins/ml[ml] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] +|This plugin provides access to the machine learning features provided by +Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md new file mode 100644 index 0000000000000..5616064ddaa0a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getValueBucketPath](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) + +## AggConfig.getValueBucketPath() method + +Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) + +Signature: + +```typescript +getValueBucketPath(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md index ceb90cffbf6ca..d4a8eddf51cfc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md @@ -47,6 +47,7 @@ export declare class AggConfig | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md) | | | | [getTimeRange()](./kibana-plugin-plugins-data-public.aggconfig.gettimerange.md) | | | | [getValue(bucket)](./kibana-plugin-plugins-data-public.aggconfig.getvalue.md) | | | +| [getValueBucketPath()](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) | | Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) | | [isFilterable()](./kibana-plugin-plugins-data-public.aggconfig.isfilterable.md) | | | | [makeLabel(percentageMode)](./kibana-plugin-plugins-data-public.aggconfig.makelabel.md) | | | | [nextId(list)](./kibana-plugin-plugins-data-public.aggconfig.nextid.md) | static | Calculate the next id based on the ids in this list {array} list - a list of objects with id properties | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md deleted file mode 100644 index 6c8594b7eeffd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) - -## expandShorthand variable - - -Signature: - -```typescript -expandShorthand: (sh: Record) => MappingObject -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md deleted file mode 100644 index 3e8b0abec529c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) - -## FieldMappingSpec.\_deserialize property - -Signature: - -```typescript -_deserialize?: (mapping: string) => any | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md deleted file mode 100644 index d0aaf7ddd0c17..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) - -## FieldMappingSpec.\_serialize property - -Signature: - -```typescript -_serialize?: (mapping: any) => string | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md deleted file mode 100644 index 38ebe60df99a1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) - -## FieldMappingSpec interface - - -Signature: - -```typescript -export interface FieldMappingSpec -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) | (mapping: string) => any | undefined | | -| [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) | (mapping: any) => string | undefined | | -| [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) | ES_FIELD_TYPES | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md deleted file mode 100644 index 73cff623dc7f2..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) - -## FieldMappingSpec.type property - -Signature: - -```typescript -type: ES_FIELD_TYPES; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md index 1fe551def29ba..5cfd5e1bc9929 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md @@ -7,10 +7,10 @@ Signature: ```typescript -export declare type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { +export declare type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, 'placeholder'> & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; - savedObjectsClient: SavedObjectsClientContract; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md deleted file mode 100644 index b1f33c8e8546d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) - -## MappingObject type - - -Signature: - -```typescript -export declare type MappingObject = Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index f8897a059377d..6a3c437305cc8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -59,7 +59,6 @@ | [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | Data plugin prewired UI components | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | -| [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | @@ -108,7 +107,6 @@ | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | -| [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | @@ -163,7 +161,6 @@ | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | -| [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md index d35dc3aa11000..e7c331bad64e8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 9c47ea1a166d5..b99c5f0f10a9e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -16,6 +16,6 @@ export interface ISearchStartAggsStart | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | -| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | ISearchStrategy['search'] | | | [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md index fdcd4d6768db5..98ea175aaaea7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; +search: ISearchStrategy['search']; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index 3d2caf417f3cb..6dd95da2be3c1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -17,5 +17,5 @@ export interface ISearchStrategy(context: RequestHandlerContext, id: string) => Promise<void> | | -| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable<SearchStrategyResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md index 45f43648ab603..84b90ae23f916 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; +search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index e44cb5c657747..215eac9829451 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md new file mode 100644 index 0000000000000..9504d50cf92d3 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [ATTRIBUTE\_SERVICE\_KEY](./kibana-plugin-plugins-embeddable-public.attribute_service_key.md) + +## ATTRIBUTE\_SERVICE\_KEY variable + +The attribute service is a shared, generic service that embeddables can use to provide the functionality required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute\_service can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object into an embeddable input shape that contains that saved object's attributes by value. + +Signature: + +```typescript +ATTRIBUTE_SERVICE_KEY = "attributes" +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md new file mode 100644 index 0000000000000..930250be2018b --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [(constructor)](./kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md) + +## AttributeService.(constructor) + +Constructs a new instance of the `AttributeService` class + +Signature: + +```typescript +constructor(type: string, showSaveModal: (saveModal: React.ReactElement, I18nContext: I18nStart['Context']) => void, i18nContext: I18nStart['Context'], toasts: NotificationsStart['toasts'], options: AttributeServiceOptions, getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| showSaveModal | (saveModal: React.ReactElement, I18nContext: I18nStart['Context']) => void | | +| i18nContext | I18nStart['Context'] | | +| toasts | NotificationsStart['toasts'] | | +| options | AttributeServiceOptions<SavedObjectAttributes> | | +| getEmbeddableFactory | (embeddableFactoryId: string) => EmbeddableFactory | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md new file mode 100644 index 0000000000000..e3f27723e1a70 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getExplicitInputFromEmbeddable](./kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md) + +## AttributeService.getExplicitInputFromEmbeddable() method + +Signature: + +```typescript +getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| embeddable | IEmbeddable | | + +Returns: + +`ValType | RefType` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md new file mode 100644 index 0000000000000..7908327c594d8 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getInputAsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md) + +## AttributeService.getInputAsRefType property + +Signature: + +```typescript +getInputAsRefType: (input: ValType | RefType, saveOptions?: { + showSaveModal: boolean; + saveModalTitle?: string | undefined; + } | { + title: string; + } | undefined) => Promise; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md new file mode 100644 index 0000000000000..939194575cbb7 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getInputAsValueType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md) + +## AttributeService.getInputAsValueType property + +Signature: + +```typescript +getInputAsValueType: (input: ValType | RefType) => Promise; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md new file mode 100644 index 0000000000000..c17ad97c3eeed --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [inputIsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md) + +## AttributeService.inputIsRefType property + +Signature: + +```typescript +inputIsRefType: (input: ValType | RefType) => input is RefType; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md new file mode 100644 index 0000000000000..b63516c909d3c --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md @@ -0,0 +1,40 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) + +## AttributeService class + +Signature: + +```typescript +export declare class AttributeService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(type, showSaveModal, i18nContext, toasts, options, getEmbeddableFactory)](./kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md) | | Constructs a new instance of the AttributeService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [getInputAsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md) | | (input: ValType | RefType, saveOptions?: {
showSaveModal: boolean;
saveModalTitle?: string | undefined;
} | {
title: string;
} | undefined) => Promise<RefType> | | +| [getInputAsValueType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md) | | (input: ValType | RefType) => Promise<ValType> | | +| [inputIsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md) | | (input: ValType | RefType) => input is RefType | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getExplicitInputFromEmbeddable(embeddable)](./kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md) | | | +| [unwrapAttributes(input)](./kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md) | | | +| [wrapAttributes(newAttributes, useRefType, input)](./kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md) | | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md new file mode 100644 index 0000000000000..f08736a2240a3 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [unwrapAttributes](./kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md) + +## AttributeService.unwrapAttributes() method + +Signature: + +```typescript +unwrapAttributes(input: RefType | ValType): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| input | RefType | ValType | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md new file mode 100644 index 0000000000000..e22a2ec3faeb4 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [wrapAttributes](./kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md) + +## AttributeService.wrapAttributes() method + +Signature: + +```typescript +wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newAttributes | SavedObjectAttributes | | +| useRefType | boolean | | +| input | ValType | RefType | | + +Returns: + +`Promise>` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md new file mode 100644 index 0000000000000..ca75b756d199e --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableStart](./kibana-plugin-plugins-embeddable-public.embeddablestart.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md) + +## EmbeddableStart.getAttributeService property + +Signature: + +```typescript +getAttributeService: (type: string, options: AttributeServiceOptions) => AttributeService; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md index f8e0028d8344b..541575566d3f7 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md @@ -15,6 +15,7 @@ export interface EmbeddableStart extends PersistableState | Property | Type | Description | | --- | --- | --- | | [EmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.embeddablepanel.md) | EmbeddablePanelHOC | | +| [getAttributeService](./kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md) | <A extends {
title: string;
}, V extends EmbeddableInput & {
[ATTRIBUTE_SERVICE_KEY]: A;
} = EmbeddableInput & {
[ATTRIBUTE_SERVICE_KEY]: A;
}, R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput>(type: string, options: AttributeServiceOptions<A>) => AttributeService<A, V, R> | | | [getEmbeddableFactories](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactories.md) | () => IterableIterator<EmbeddableFactory> | | | [getEmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactory.md) | <I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable<I, O> = IEmbeddable<I, O>>(embeddableFactoryId: string) => EmbeddableFactory<I, O, E> | undefined | | | [getEmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablepanel.md) | (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index 64dfdd1c6dc22..df67eda5074b9 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -9,6 +9,7 @@ | Class | Description | | --- | --- | | [AddPanelAction](./kibana-plugin-plugins-embeddable-public.addpanelaction.md) | | +| [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) | | | [Container](./kibana-plugin-plugins-embeddable-public.container.md) | | | [EditPanelAction](./kibana-plugin-plugins-embeddable-public.editpanelaction.md) | | | [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) | | @@ -71,6 +72,7 @@ | --- | --- | | [ACTION\_ADD\_PANEL](./kibana-plugin-plugins-embeddable-public.action_add_panel.md) | | | [ACTION\_EDIT\_PANEL](./kibana-plugin-plugins-embeddable-public.action_edit_panel.md) | | +| [ATTRIBUTE\_SERVICE\_KEY](./kibana-plugin-plugins-embeddable-public.attribute_service_key.md) | The attribute service is a shared, generic service that embeddables can use to provide the functionality required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute\_service can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object into an embeddable input shape that contains that saved object's attributes by value. | | [CONTEXT\_MENU\_TRIGGER](./kibana-plugin-plugins-embeddable-public.context_menu_trigger.md) | | | [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | | | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md new file mode 100644 index 0000000000000..9cd77ca6e3a36 --- /dev/null +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) + +## EmbeddableSetup.getAttributeService property + +Signature: + +```typescript +getAttributeService: any; +``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index 59ca4a2bbca75..bd024095e80be 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -14,6 +14,7 @@ export interface EmbeddableSetup | Property | Type | Description | | --- | --- | --- | +| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md new file mode 100644 index 0000000000000..6f30bd49013b0 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [extract](./kibana-plugin-plugins-expressions-public.executor.extract.md) + +## Executor.extract() method + +Signature: + +```typescript +extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | + +Returns: + +`{ + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md new file mode 100644 index 0000000000000..8f5a8a3e06724 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [inject](./kibana-plugin-plugins-expressions-public.executor.inject.md) + +## Executor.inject() method + +Signature: + +```typescript +inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| references | SavedObjectReference[] | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index b71c8c79c068f..2f96ad6e040bd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> +export declare class Executor = Record> implements PersistableState ``` ## Constructors @@ -32,12 +32,15 @@ export declare class Executor = Recordstatic | | | [extendContext(extraContext)](./kibana-plugin-plugins-expressions-public.executor.extendcontext.md) | | | +| [extract(ast)](./kibana-plugin-plugins-expressions-public.executor.extract.md) | | | | [fork()](./kibana-plugin-plugins-expressions-public.executor.fork.md) | | | | [getFunction(name)](./kibana-plugin-plugins-expressions-public.executor.getfunction.md) | | | | [getFunctions()](./kibana-plugin-plugins-expressions-public.executor.getfunctions.md) | | | | [getType(name)](./kibana-plugin-plugins-expressions-public.executor.gettype.md) | | | | [getTypes()](./kibana-plugin-plugins-expressions-public.executor.gettypes.md) | | | +| [inject(ast, references)](./kibana-plugin-plugins-expressions-public.executor.inject.md) | | | | [registerFunction(functionDefinition)](./kibana-plugin-plugins-expressions-public.executor.registerfunction.md) | | | | [registerType(typeDefinition)](./kibana-plugin-plugins-expressions-public.executor.registertype.md) | | | | [run(ast, input, context)](./kibana-plugin-plugins-expressions-public.executor.run.md) | | Execute expression and return result. | +| [telemetry(ast, telemetryData)](./kibana-plugin-plugins-expressions-public.executor.telemetry.md) | | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md new file mode 100644 index 0000000000000..de4c640f4fe97 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [telemetry](./kibana-plugin-plugins-expressions-public.executor.telemetry.md) + +## Executor.telemetry() method + +Signature: + +```typescript +telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| telemetryData | Record<string, any> | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md deleted file mode 100644 index b50ac83036ffe..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) > [chain](./kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md) - -## ExpressionAstExpression.chain property - -Signature: - -```typescript -chain: ExpressionAstFunction[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md index 537659c51dce8..623c49bf08cdd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md @@ -2,18 +2,13 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) -## ExpressionAstExpression interface +## ExpressionAstExpression type Signature: ```typescript -export interface ExpressionAstExpression +export declare type ExpressionAstExpression = { + type: 'expression'; + chain: ExpressionAstFunction[]; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [chain](./kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md) | ExpressionAstFunction[] | | -| [type](./kibana-plugin-plugins-expressions-public.expressionastexpression.type.md) | 'expression' | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md deleted file mode 100644 index 72b44e8319542..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [arguments](./kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md) - -## ExpressionAstFunction.arguments property - -Signature: - -```typescript -arguments: Record; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md deleted file mode 100644 index 36101a110979a..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [debug](./kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md) - -## ExpressionAstFunction.debug property - -Debug information added to each function when expression is executed in \*debug mode\*. - -Signature: - -```typescript -debug?: ExpressionAstFunctionDebug; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md deleted file mode 100644 index 1840fff4b625f..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [function](./kibana-plugin-plugins-expressions-public.expressionastfunction.function.md) - -## ExpressionAstFunction.function property - -Signature: - -```typescript -function: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md index 1004e58759806..d21f2c1750161 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) -## ExpressionAstFunction interface +## ExpressionAstFunction type Signature: ```typescript -export interface ExpressionAstFunction +export declare type ExpressionAstFunction = { + type: 'function'; + function: string; + arguments: Record; + debug?: ExpressionAstFunctionDebug; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [arguments](./kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md) | Record<string, ExpressionAstArgument[]> | | -| [debug](./kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md) | ExpressionAstFunctionDebug | Debug information added to each function when expression is executed in \*debug mode\*. | -| [function](./kibana-plugin-plugins-expressions-public.expressionastfunction.function.md) | string | | -| [type](./kibana-plugin-plugins-expressions-public.expressionastfunction.type.md) | 'function' | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md similarity index 52% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md index f7f8786430191..f07d5b3b36d04 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [type](./kibana-plugin-plugins-expressions-public.expressionastfunction.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [disabled](./kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md) -## ExpressionAstFunction.type property +## ExpressionFunction.disabled property Signature: ```typescript -type: 'function'; +disabled: boolean; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md new file mode 100644 index 0000000000000..c5d726849cdc2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [extract](./kibana-plugin-plugins-expressions-public.expressionfunction.extract.md) + +## ExpressionFunction.extract property + +Signature: + +```typescript +extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md new file mode 100644 index 0000000000000..6f27a6fbab96a --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) + +## ExpressionFunction.inject property + +Signature: + +```typescript +inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 5ca67e40c93ec..1815d63d804b1 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class ExpressionFunction +export declare class ExpressionFunction implements PersistableState ``` ## Constructors @@ -23,9 +23,13 @@ export declare class ExpressionFunction | [accepts](./kibana-plugin-plugins-expressions-public.expressionfunction.accepts.md) | | (type: string) => boolean | | | [aliases](./kibana-plugin-plugins-expressions-public.expressionfunction.aliases.md) | | string[] | Aliases that can be used instead of name. | | [args](./kibana-plugin-plugins-expressions-public.expressionfunction.args.md) | | Record<string, ExpressionFunctionParameter> | Specification of expression function parameters. | +| [disabled](./kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md) | | boolean | | +| [extract](./kibana-plugin-plugins-expressions-public.expressionfunction.extract.md) | | (state: ExpressionAstFunction['arguments']) => {
state: ExpressionAstFunction['arguments'];
references: SavedObjectReference[];
} | | | [fn](./kibana-plugin-plugins-expressions-public.expressionfunction.fn.md) | | (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue | Function to run function (context, args) | | [help](./kibana-plugin-plugins-expressions-public.expressionfunction.help.md) | | string | A short help text. | +| [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | +| [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | 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. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md new file mode 100644 index 0000000000000..249c99f50fc7b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) + +## ExpressionFunction.telemetry property + +Signature: + +```typescript +telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md new file mode 100644 index 0000000000000..e6aefd17fceb2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) > [disabled](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md) + +## ExpressionFunctionDefinition.disabled property + +if set to true function will be disabled (but its migrate function will still be available) + +Signature: + +```typescript +disabled?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md index bc801542f81ac..449cc66cb3335 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition ``` ## Properties @@ -19,6 +19,7 @@ export interface ExpressionFunctionDefinitionstring[] | What is this? | | [args](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.args.md) | {
[key in keyof Arguments]: ArgumentType<Arguments[key]>;
} | Specification of arguments that function supports. This list will also be used for autocomplete functionality when your function is being edited. | | [context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.context.md) | {
types: AnyExpressionFunctionDefinition['inputTypes'];
} | | +| [disabled](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md) | boolean | if set to true function will be disabled (but its migrate function will still be available) | | [help](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.help.md) | string | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | 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. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md index 3b3c1644adbef..9a2507056eb80 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md @@ -14,5 +14,6 @@ export interface ExpressionRenderError extends Error | Property | Type | Description | | --- | --- | --- | +| [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) | Error | | | [type](./kibana-plugin-plugins-expressions-public.expressionrendererror.type.md) | string | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md similarity index 51% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md index 34a86e235a911..45f74a52e6b6f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) > [type](./kibana-plugin-plugins-expressions-public.expressionastexpression.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionRenderError](./kibana-plugin-plugins-expressions-public.expressionrendererror.md) > [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) -## ExpressionAstExpression.type property +## ExpressionRenderError.original property Signature: ```typescript -type: 'expression'; +original?: Error; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md new file mode 100644 index 0000000000000..90f1f59c90dea --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [extract](./kibana-plugin-plugins-expressions-public.expressionsservice.extract.md) + +## ExpressionsService.extract property + +Extracts saved object references from expression AST + +Signature: + +```typescript +readonly extract: (state: ExpressionAstExpression) => { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md new file mode 100644 index 0000000000000..8ccc673ef24db --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) + +## ExpressionsService.inject property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index fa93435bffc38..041d66b22dd50 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -15,7 +15,7 @@ so that JSDoc appears in developers IDE when they use those `plugins.expressions Signature: ```typescript -export declare class ExpressionsService +export declare class ExpressionsService implements PersistableState ``` ## Constructors @@ -30,6 +30,7 @@ export declare class ExpressionsService | --- | --- | --- | --- | | [execute](./kibana-plugin-plugins-expressions-public.expressionsservice.execute.md) | | ExpressionsServiceStart['execute'] | | | [executor](./kibana-plugin-plugins-expressions-public.expressionsservice.executor.md) | | Executor | | +| [extract](./kibana-plugin-plugins-expressions-public.expressionsservice.extract.md) | | (state: ExpressionAstExpression) => {
state: ExpressionAstExpression;
references: SavedObjectReference[];
} | Extracts saved object references from expression AST | | [fork](./kibana-plugin-plugins-expressions-public.expressionsservice.fork.md) | | () => ExpressionsService | | | [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.getfunction.md) | | ExpressionsServiceStart['getFunction'] | | | [getFunctions](./kibana-plugin-plugins-expressions-public.expressionsservice.getfunctions.md) | | () => ReturnType<Executor['getFunctions']> | Returns POJO map of all registered expression functions, where keys are names of the functions and values are ExpressionFunction instances. | @@ -37,6 +38,7 @@ export declare class ExpressionsService | [getRenderers](./kibana-plugin-plugins-expressions-public.expressionsservice.getrenderers.md) | | () => ReturnType<ExpressionRendererRegistry['toJS']> | Returns POJO map of all registered expression renderers, where keys are names of the renderers and values are ExpressionRenderer instances. | | [getType](./kibana-plugin-plugins-expressions-public.expressionsservice.gettype.md) | | ExpressionsServiceStart['getType'] | | | [getTypes](./kibana-plugin-plugins-expressions-public.expressionsservice.gettypes.md) | | () => ReturnType<Executor['getTypes']> | Returns POJO map of all registered expression types, where keys are names of the types and values are ExpressionType instances. | +| [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) | | (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression | Injects saved object references into expression AST | | [registerFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.registerfunction.md) | | (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void | 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({ @@ -61,6 +63,7 @@ The actual function is defined in the fn key. The function can be \ | [registerType](./kibana-plugin-plugins-expressions-public.expressionsservice.registertype.md) | | (typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)) => void | | | [renderers](./kibana-plugin-plugins-expressions-public.expressionsservice.renderers.md) | | ExpressionRendererRegistry | | | [run](./kibana-plugin-plugins-expressions-public.expressionsservice.run.md) | | ExpressionsServiceStart['run'] | | +| [telemetry](./kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md) | | (state: ExpressionAstExpression, telemetryData?: Record<string, any>) => Record<string, any> | Extracts telemetry from expression AST | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md new file mode 100644 index 0000000000000..5f28eb732e389 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [telemetry](./kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md) + +## ExpressionsService.telemetry property + +Extracts telemetry from expression AST + +Signature: + +```typescript +readonly telemetry: (state: ExpressionAstExpression, telemetryData?: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md index 4a714fe62424f..1dee4a139c660 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md @@ -8,13 +8,7 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; }>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md index 9dbd18ae687b4..ab0273be71402 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md @@ -18,5 +18,6 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void | | +| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | PersistedState | | | [update](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.update.md) | (params: any) => void | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md new file mode 100644 index 0000000000000..8d74c8e555fee --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) + +## IInterpreterRenderHandlers.uiState property + +Signature: + +```typescript +uiState?: PersistedState; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index ead6f14e0d1d7..b0c732188a46e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -55,9 +55,7 @@ | [ExecutionParams](./kibana-plugin-plugins-expressions-public.executionparams.md) | | | [ExecutionState](./kibana-plugin-plugins-expressions-public.executionstate.md) | | | [ExecutorState](./kibana-plugin-plugins-expressions-public.executorstate.md) | | -| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) | | | [ExpressionAstExpressionBuilder](./kibana-plugin-plugins-expressions-public.expressionastexpressionbuilder.md) | | -| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) | | | [ExpressionAstFunctionBuilder](./kibana-plugin-plugins-expressions-public.expressionastfunctionbuilder.md) | | | [ExpressionExecutor](./kibana-plugin-plugins-expressions-public.expressionexecutor.md) | | | [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) | ExpressionFunctionDefinition is the interface plugins have to implement to register a function in expressions plugin. | @@ -102,6 +100,8 @@ | [ExecutionContainer](./kibana-plugin-plugins-expressions-public.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-public.executorcontainer.md) | | | [ExpressionAstArgument](./kibana-plugin-plugins-expressions-public.expressionastargument.md) | | +| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) | | +| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-public.expressionastnode.md) | | | [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md) | | | [ExpressionRendererComponent](./kibana-plugin-plugins-expressions-public.expressionrenderercomponent.md) | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md new file mode 100644 index 0000000000000..26d1e7810f9e7 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Range](./kibana-plugin-plugins-expressions-public.range.md) > [label](./kibana-plugin-plugins-expressions-public.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md index cf0cf4cb50b71..83d4b9bd35090 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-public.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-public.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-public.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-public.range.type.md) | typeof name | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index bd6c8cba5f784..5622516530edd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -20,5 +20,5 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | | [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | Observable<unknown> | An observable which can be used to re-run the expression without destroying the component | -| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (error?: string | null) => React.ReactElement | React.ReactElement[] | | +| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[] | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md index 48bfe1ee5c7c7..162d0da04ae7f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md @@ -7,5 +7,5 @@ Signature: ```typescript -renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; +renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md new file mode 100644 index 0000000000000..0829824732e74 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [extract](./kibana-plugin-plugins-expressions-server.executor.extract.md) + +## Executor.extract() method + +Signature: + +```typescript +extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | + +Returns: + +`{ + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md new file mode 100644 index 0000000000000..bbc5f7a3cece7 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [inject](./kibana-plugin-plugins-expressions-server.executor.inject.md) + +## Executor.inject() method + +Signature: + +```typescript +inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| references | SavedObjectReference[] | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index 7e6bb8c7ded5e..ec4e0bdcc4569 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> +export declare class Executor = Record> implements PersistableState ``` ## Constructors @@ -32,12 +32,15 @@ export declare class Executor = Recordstatic | | | [extendContext(extraContext)](./kibana-plugin-plugins-expressions-server.executor.extendcontext.md) | | | +| [extract(ast)](./kibana-plugin-plugins-expressions-server.executor.extract.md) | | | | [fork()](./kibana-plugin-plugins-expressions-server.executor.fork.md) | | | | [getFunction(name)](./kibana-plugin-plugins-expressions-server.executor.getfunction.md) | | | | [getFunctions()](./kibana-plugin-plugins-expressions-server.executor.getfunctions.md) | | | | [getType(name)](./kibana-plugin-plugins-expressions-server.executor.gettype.md) | | | | [getTypes()](./kibana-plugin-plugins-expressions-server.executor.gettypes.md) | | | +| [inject(ast, references)](./kibana-plugin-plugins-expressions-server.executor.inject.md) | | | | [registerFunction(functionDefinition)](./kibana-plugin-plugins-expressions-server.executor.registerfunction.md) | | | | [registerType(typeDefinition)](./kibana-plugin-plugins-expressions-server.executor.registertype.md) | | | | [run(ast, input, context)](./kibana-plugin-plugins-expressions-server.executor.run.md) | | Execute expression and return result. | +| [telemetry(ast, telemetryData)](./kibana-plugin-plugins-expressions-server.executor.telemetry.md) | | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md new file mode 100644 index 0000000000000..68100c38cfa5b --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [telemetry](./kibana-plugin-plugins-expressions-server.executor.telemetry.md) + +## Executor.telemetry() method + +Signature: + +```typescript +telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| telemetryData | Record<string, any> | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md deleted file mode 100644 index cc8006b918dec..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) > [chain](./kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md) - -## ExpressionAstExpression.chain property - -Signature: - -```typescript -chain: ExpressionAstFunction[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md index b5f83d1af7cb7..9606cb9e36960 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md @@ -2,18 +2,13 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) -## ExpressionAstExpression interface +## ExpressionAstExpression type Signature: ```typescript -export interface ExpressionAstExpression +export declare type ExpressionAstExpression = { + type: 'expression'; + chain: ExpressionAstFunction[]; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [chain](./kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md) | ExpressionAstFunction[] | | -| [type](./kibana-plugin-plugins-expressions-server.expressionastexpression.type.md) | 'expression' | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md deleted file mode 100644 index 46cd60cecaa84..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) > [type](./kibana-plugin-plugins-expressions-server.expressionastexpression.type.md) - -## ExpressionAstExpression.type property - -Signature: - -```typescript -type: 'expression'; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md deleted file mode 100644 index 052cadffb9bdb..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [arguments](./kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md) - -## ExpressionAstFunction.arguments property - -Signature: - -```typescript -arguments: Record; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md deleted file mode 100644 index b3227c2ac5822..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [debug](./kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md) - -## ExpressionAstFunction.debug property - -Debug information added to each function when expression is executed in \*debug mode\*. - -Signature: - -```typescript -debug?: ExpressionAstFunctionDebug; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md deleted file mode 100644 index 9964409f49119..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [function](./kibana-plugin-plugins-expressions-server.expressionastfunction.function.md) - -## ExpressionAstFunction.function property - -Signature: - -```typescript -function: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md index 1d49de44b571d..7fbcf2dcfd141 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) -## ExpressionAstFunction interface +## ExpressionAstFunction type Signature: ```typescript -export interface ExpressionAstFunction +export declare type ExpressionAstFunction = { + type: 'function'; + function: string; + arguments: Record; + debug?: ExpressionAstFunctionDebug; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [arguments](./kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md) | Record<string, ExpressionAstArgument[]> | | -| [debug](./kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md) | ExpressionAstFunctionDebug | Debug information added to each function when expression is executed in \*debug mode\*. | -| [function](./kibana-plugin-plugins-expressions-server.expressionastfunction.function.md) | string | | -| [type](./kibana-plugin-plugins-expressions-server.expressionastfunction.type.md) | 'function' | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md similarity index 52% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md index 3fd10524c1599..8ae51645f5df9 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [type](./kibana-plugin-plugins-expressions-server.expressionastfunction.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [disabled](./kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md) -## ExpressionAstFunction.type property +## ExpressionFunction.disabled property Signature: ```typescript -type: 'function'; +disabled: boolean; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md new file mode 100644 index 0000000000000..e7ecad4a6c9e4 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [extract](./kibana-plugin-plugins-expressions-server.expressionfunction.extract.md) + +## ExpressionFunction.extract property + +Signature: + +```typescript +extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md new file mode 100644 index 0000000000000..85c98ef9193da --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) + +## ExpressionFunction.inject property + +Signature: + +```typescript +inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index aac3878b8c859..7fcda94968d13 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class ExpressionFunction +export declare class ExpressionFunction implements PersistableState ``` ## Constructors @@ -23,9 +23,13 @@ export declare class ExpressionFunction | [accepts](./kibana-plugin-plugins-expressions-server.expressionfunction.accepts.md) | | (type: string) => boolean | | | [aliases](./kibana-plugin-plugins-expressions-server.expressionfunction.aliases.md) | | string[] | Aliases that can be used instead of name. | | [args](./kibana-plugin-plugins-expressions-server.expressionfunction.args.md) | | Record<string, ExpressionFunctionParameter> | Specification of expression function parameters. | +| [disabled](./kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md) | | boolean | | +| [extract](./kibana-plugin-plugins-expressions-server.expressionfunction.extract.md) | | (state: ExpressionAstFunction['arguments']) => {
state: ExpressionAstFunction['arguments'];
references: SavedObjectReference[];
} | | | [fn](./kibana-plugin-plugins-expressions-server.expressionfunction.fn.md) | | (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue | Function to run function (context, args) | | [help](./kibana-plugin-plugins-expressions-server.expressionfunction.help.md) | | string | A short help text. | +| [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | +| [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | 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. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md new file mode 100644 index 0000000000000..2894486847b27 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) + +## ExpressionFunction.telemetry property + +Signature: + +```typescript +telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md new file mode 100644 index 0000000000000..88456c8700aec --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md) > [disabled](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md) + +## ExpressionFunctionDefinition.disabled property + +if set to true function will be disabled (but its migrate function will still be available) + +Signature: + +```typescript +disabled?: boolean; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md index 6463c6ac537b9..51240f094b181 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition ``` ## Properties @@ -19,6 +19,7 @@ export interface ExpressionFunctionDefinitionstring[] | What is this? | | [args](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.args.md) | {
[key in keyof Arguments]: ArgumentType<Arguments[key]>;
} | Specification of arguments that function supports. This list will also be used for autocomplete functionality when your function is being edited. | | [context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.context.md) | {
types: AnyExpressionFunctionDefinition['inputTypes'];
} | | +| [disabled](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md) | boolean | if set to true function will be disabled (but its migrate function will still be available) | | [help](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.help.md) | string | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | 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. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md index b90e4360e055a..c8132948a8993 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md @@ -8,13 +8,7 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; }>; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md index cbaffa04bae8f..ccf6271f712b9 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md @@ -18,5 +18,6 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void | | +| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | PersistedState | | | [update](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.update.md) | (params: any) => void | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md new file mode 100644 index 0000000000000..b09433c6454ad --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) + +## IInterpreterRenderHandlers.uiState property + +Signature: + +```typescript +uiState?: PersistedState; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md index c9fed2e00c66c..dd7c7af466bd0 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md @@ -52,9 +52,7 @@ | [ExecutionParams](./kibana-plugin-plugins-expressions-server.executionparams.md) | | | [ExecutionState](./kibana-plugin-plugins-expressions-server.executionstate.md) | | | [ExecutorState](./kibana-plugin-plugins-expressions-server.executorstate.md) | | -| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) | | | [ExpressionAstExpressionBuilder](./kibana-plugin-plugins-expressions-server.expressionastexpressionbuilder.md) | | -| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) | | | [ExpressionAstFunctionBuilder](./kibana-plugin-plugins-expressions-server.expressionastfunctionbuilder.md) | | | [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md) | ExpressionFunctionDefinition is the interface plugins have to implement to register a function in expressions plugin. | | [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) | A mapping of ExpressionFunctionDefinitions for functions which the Expressions services provides out-of-the-box. Any new functions registered by the Expressions plugin should have their types added here. | @@ -86,6 +84,8 @@ | [ExecutionContainer](./kibana-plugin-plugins-expressions-server.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-server.executorcontainer.md) | | | [ExpressionAstArgument](./kibana-plugin-plugins-expressions-server.expressionastargument.md) | | +| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) | | +| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-server.expressionastnode.md) | | | [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md) | | | [ExpressionsServerSetup](./kibana-plugin-plugins-expressions-server.expressionsserversetup.md) | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md new file mode 100644 index 0000000000000..767f6011290a1 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Range](./kibana-plugin-plugins-expressions-server.range.md) > [label](./kibana-plugin-plugins-expressions-server.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md index d369d882757fc..4e6ae12217f2e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-server.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-server.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-server.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-server.range.type.md) | typeof name | | diff --git a/docs/ingest_manager/ingest-manager.asciidoc b/docs/fleet/fleet.asciidoc similarity index 65% rename from docs/ingest_manager/ingest-manager.asciidoc rename to docs/fleet/fleet.asciidoc index 8f6e8036c68cd..06b2b96c0035c 100644 --- a/docs/ingest_manager/ingest-manager.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -1,11 +1,11 @@ [chapter] [role="xpack"] -[[ingest-manager]] -= {ingest-manager} +[[fleet]] += {fleet} -experimental[] +beta[] -{ingest-manager} in {kib} enables you to add and manage integrations for popular +{fleet} in {kib} enables you to add and manage integrations for popular services and platforms, as well as manage {elastic-agent} installations in standalone or {fleet} mode. @@ -17,11 +17,13 @@ Standalone mode requires you to manually configure and manage the agent locally. * An overview of the data ingest in your {es} cluster. * Multiple integrations to collect and transform data. +//TODO: Redo screen capture. + [role="screenshot"] -image::ingest_manager/images/ingest-manager-start.png[{ingest-manager} app in {kib}] +image::fleet/images/fleet-start.png[{fleet} app in {kib}] [float] == Get started -To get started with {ingest-management}, refer to the +To get started with {fleet}, refer to the {ingest-guide}/index.html[Ingest Management Guide]. diff --git a/docs/fleet/images/fleet-start.png b/docs/fleet/images/fleet-start.png new file mode 100644 index 0000000000000..60e5416fde127 Binary files /dev/null and b/docs/fleet/images/fleet-start.png differ diff --git a/docs/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png index 1878550bc3169..b8c2002b9c4cd 100644 Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ diff --git a/docs/getting-started/images/gs_maps_time_filter.png b/docs/getting-started/images/gs_maps_time_filter.png deleted file mode 100644 index 83e20c279906e..0000000000000 Binary files a/docs/getting-started/images/gs_maps_time_filter.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-dashboard.png b/docs/getting-started/images/tutorial-dashboard.png deleted file mode 100644 index 8193d410bc5f1..0000000000000 Binary files a/docs/getting-started/images/tutorial-dashboard.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png index 681e4834de830..cf217562c37fd 100644 Binary files a/docs/getting-started/images/tutorial-discover-2.png and b/docs/getting-started/images/tutorial-discover-2.png differ diff --git a/docs/getting-started/images/tutorial-discover-3.png b/docs/getting-started/images/tutorial-discover-3.png index bbab47acaf9d4..b024ad6dc39fe 100644 Binary files a/docs/getting-started/images/tutorial-discover-3.png and b/docs/getting-started/images/tutorial-discover-3.png differ diff --git a/docs/getting-started/images/tutorial-discover-4.png b/docs/getting-started/images/tutorial-discover-4.png new file mode 100644 index 0000000000000..945a6155c02cd Binary files /dev/null and b/docs/getting-started/images/tutorial-discover-4.png differ diff --git a/docs/getting-started/images/tutorial-final-dashboard.gif b/docs/getting-started/images/tutorial-final-dashboard.gif new file mode 100644 index 0000000000000..53b7bc04c5f65 Binary files /dev/null and b/docs/getting-started/images/tutorial-final-dashboard.gif differ diff --git a/docs/getting-started/images/tutorial-full-inspect1.png b/docs/getting-started/images/tutorial-full-inspect1.png deleted file mode 100644 index 94c9f2566f624..0000000000000 Binary files a/docs/getting-started/images/tutorial-full-inspect1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png deleted file mode 100644 index 0026b18775518..0000000000000 Binary files a/docs/getting-started/images/tutorial-pattern-1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png index ccce8c3bb3208..9f287640f201c 100644 Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ diff --git a/docs/getting-started/images/tutorial-sample-discover1.png b/docs/getting-started/images/tutorial-sample-discover1.png deleted file mode 100644 index 1bad8774ba584..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-discover1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-discover2.png b/docs/getting-started/images/tutorial-sample-discover2.png deleted file mode 100644 index a439f1d76a991..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-discover2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-edit1.png b/docs/getting-started/images/tutorial-sample-edit1.png deleted file mode 100644 index b5ae56b5c0d83..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-edit1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-edit2.png b/docs/getting-started/images/tutorial-sample-edit2.png deleted file mode 100644 index 17a029a17e1b4..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-edit2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png index 770e26e951b3a..7c1d041448557 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter2.png b/docs/getting-started/images/tutorial-sample-filter2.png new file mode 100644 index 0000000000000..21402feacdecd Binary files /dev/null and b/docs/getting-started/images/tutorial-sample-filter2.png differ diff --git a/docs/getting-started/images/tutorial-sample-inspect1.png b/docs/getting-started/images/tutorial-sample-inspect1.png deleted file mode 100644 index 6a3d41ae03584..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-inspect1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-query.png b/docs/getting-started/images/tutorial-sample-query.png index 847542c0b17ff..4f1ca24924b28 100644 Binary files a/docs/getting-started/images/tutorial-sample-query.png and b/docs/getting-started/images/tutorial-sample-query.png differ diff --git a/docs/getting-started/images/tutorial-sample-query2.png b/docs/getting-started/images/tutorial-sample-query2.png new file mode 100644 index 0000000000000..0e91e1069a201 Binary files /dev/null and b/docs/getting-started/images/tutorial-sample-query2.png differ diff --git a/docs/getting-started/images/tutorial-treemap.png b/docs/getting-started/images/tutorial-treemap.png new file mode 100644 index 0000000000000..32e14fd2308e3 Binary files /dev/null and b/docs/getting-started/images/tutorial-treemap.png differ diff --git a/docs/getting-started/images/tutorial-visualization-dropdown.png b/docs/getting-started/images/tutorial-visualization-dropdown.png new file mode 100644 index 0000000000000..29d1b99700964 Binary files /dev/null and b/docs/getting-started/images/tutorial-visualization-dropdown.png differ diff --git a/docs/getting-started/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png deleted file mode 100644 index 009152f9407e4..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-bar-1.5.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png deleted file mode 100644 index ed2fd47cb27de..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-map-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png deleted file mode 100644 index af56faa3b0516..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-md-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png deleted file mode 100644 index ca8f5e92146bc..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-pie-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png deleted file mode 100644 index 59fce360096c0..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-pie-3.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-wizard-step-1.png b/docs/getting-started/images/tutorial-visualize-wizard-step-1.png deleted file mode 100644 index afc9dda648265..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-wizard-step-1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial_index_patterns.png b/docs/getting-started/images/tutorial_index_patterns.png deleted file mode 100644 index 430baf898b612..0000000000000 Binary files a/docs/getting-started/images/tutorial_index_patterns.png and /dev/null differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc new file mode 100644 index 0000000000000..6386feac5ab49 --- /dev/null +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -0,0 +1,142 @@ +[[get-started]] +== Quick start + +To quickly get up and running with {kib}, set up on Cloud, then add a sample data set that you can explore and analyze. + +When you've finished, you'll know how to: + +* <> + +* <> + +[float] +=== Before you begin +When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. For more information, refer to {ref}/security-privileges.html[Security privileges]. + +[float] +[[set-up-on-cloud]] +== Set up on cloud + +include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] + +[float] +[[gs-get-data-into-kibana]] +== Add the sample data + +Sample data sets come with sample visualizations, dashboards, and more to help you explore {kib} without adding your own data. + +. From the home page, click *Try our sample data*. + +. On the *Sample eCommerce orders* card, click *Add data*. ++ +[role="screenshot"] +image::getting-started/images/add-sample-data.png[] + +[float] +[[explore-the-data]] +== Explore the data + +*Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document. + +. Open the menu, then click *Discover*. + +. Change the <> to *Last 7 days*. ++ +[role="screenshot"] +image::images/tutorial-discover-2.png[] + +. To focus in on the documents you want to view, use the <>. In the *KQL* search field, enter: ++ +[source,text] +products.taxless_price >= 60 AND category : Women's Clothing ++ +The query returns the women's clothing orders for $60 and more. ++ +[role="screenshot"] +image::images/tutorial-discover-4.png[] + +. Hover over the list of *Available fields*, then click *+* next to the fields you want to view in the table. ++ +For example, when you add the *category* field, the table displays the product categories for the orders. ++ +[role="screenshot"] +image::images/tutorial-discover-3.png[] ++ +For more information, refer to <>. + +[float] +[[view-and-analyze-the-data]] +== View and analyze the data + +A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more. + +. Open the menu, then click *Dashboard*. + +. Click *[eCommerce] Revenue Dashboard*. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-dashboard.png[] + +[float] +[[filter-and-query-the-data]] +=== Filter the data + +To focus in on the data you want to view on the dashboard, use filters. + +. From the *Controls* visualization, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. ++ +For example, the following dashboard shows the data for women's clothing from Gnomehouse. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-filter.png[] + +. To manually add a filter, click *Add filter*, then specify the options. ++ +For example, to view the orders for Wednesday, select *day_of_week* from the *Field* dropdown, select *is* from the *Operator* dropdown, then select *Wednesday* from the *Value* dropdown. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-filter2.png[] + +. When you are done, remove the filters. ++ +For more information, refer to <>. + +[float] +[[create-a-visualization]] +=== Create a visualization + +To create a treemap that shows the top regions and manufacturers, use *Lens*, then add the treemap to the dashboard. + +. From the {kib} toolbar, click *Edit*, then click *Create new*. + +. On the *New Visualization* window, click *Lens*. + +. From the *Available fields* list, drag and drop the following fields to the visualization builder: + +* *geoip.city_name* + +* *manufacturer.keyword* ++ +. From the visualization dropdown, select *Treemap*. ++ +[role="screenshot"] +image::getting-started/images/tutorial-visualization-dropdown.png[Visualization dropdown with Treemap selected] + +. Click *Save*. + +. On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*. ++ +The treemap appears as the last visualization on the dashboard. ++ +[role="screenshot"] +image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] ++ +For more information, refer to <>. + +[float] +[[quick-start-whats-next]] +== What's next? + +If you are you ready to add your own data, refer to <>. + +If you want to ingest your data, refer to {ingest-guide}/ingest-management-getting-started.html[Quick start: Get logs and metrics into the Elastic Stack]. diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc deleted file mode 100644 index 215952c2d3595..0000000000000 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ /dev/null @@ -1,56 +0,0 @@ -[[tutorial-define-index]] -=== Define your index patterns - -Index patterns tell {kib} which {es} indices you want to explore. -An index pattern can match the name of a single index, or include a wildcard -(*) to match multiple indices. - -For example, Logstash typically creates a -series of indices in the format `logstash-YYYY.MMM.DD`. To explore all -of the log data from May 2018, you could specify the index pattern -`logstash-2018.05*`. - -[float] -==== Create the index patterns - -First you'll create index patterns for the Shakespeare data set, which has an -index named `shakespeare,` and the accounts data set, which has an index named -`bank`. These data sets don't contain time series data. - -. Open the menu, then go to *Stack Management > {kib} > Index Patterns*. - -. If this is your first index pattern, the *Create index pattern* page opens. - -. In the *Index pattern name* field, enter `shakes*`. -+ -[role="screenshot"] -image::images/tutorial-pattern-1.png[Image showing how to enter shakes* in Index Pattern Name field] - -. Click *Next step*. - -. On the *Configure settings* page, *Create index pattern*. -+ -You’re presented a table of all fields and associated data types in the index. - -. Create a second index pattern named `ba*`. - -[float] -==== Create an index pattern for the time series data - -Create an index pattern for the Logstash index, which -contains the time series data. - -. Create an index pattern named `logstash*`, then click *Next step*. - -. From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. -+ -[role="screenshot"] -image::images/tutorial_index_patterns.png[Image showing how to create an index pattern] - -NOTE: When you define an index pattern, the indices that match that pattern must -exist in Elasticsearch and they must contain data. To check if the indices are -available, open the menu, go to *Dev Tools > Console*, then enter `GET _cat/indices`. Alternately, use -`curl -XGET "http://localhost:9200/_cat/indices"`. -For Windows, run `Invoke-RestMethod -Uri "http://localhost:9200/_cat/indices"` in Powershell. - - diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc deleted file mode 100644 index 99a07acf98791..0000000000000 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ /dev/null @@ -1,35 +0,0 @@ -[[explore-your-data]] -=== Explore your data - -With *Discover*, you use {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch -queries] to explore your data and narrow the results with filters. - -. Open the menu, then go to *Discover*. -+ -The `shakes*` index pattern appears. - -. To make `ba*` the index, click the *Change Index Pattern* dropdown, then select `ba*`. -+ -By default, all fields are shown for each matching document. - -. In the *Search* field, enter the following, then click *Update*: -+ -[source,text] -account_number<100 AND balance>47500 -+ -The search returns all account numbers between zero and 99 with balances in -excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. -+ -[role="screenshot"] -image::images/tutorial-discover-2.png[Image showing the search results for account numbers between zero and 99, with balances in excess of 47,500] -+ -. Hover over the list of *Available fields*, then -click *Add* next to each field you want include in the table. -+ -For example, when you add the `account_number` field, the display changes to a list of five -account numbers. -+ -[role="screenshot"] -image::images/tutorial-discover-3.png[Image showing a dropdown with five account numbers, which match the previous query for account balance] - -Now that you know what your documents contain, it's time to gain insight into your data with visualizations. diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc deleted file mode 100644 index a7d5412ae0632..0000000000000 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ /dev/null @@ -1,219 +0,0 @@ -[[create-your-own-dashboard]] -== Create your own dashboard - -Ready to add data to {kib} and create your own dashboard? In this tutorial, you'll use three types of data sets that'll help you learn to: - -* <> -* <> -* <> -* <> - -[float] -[[download-the-data]] -=== Download the data - -To complete the tutorial, you'll download and use the following data sets: - -* The complete works of William Shakespeare, suitably parsed into fields -* A set of fictitious bank accounts with randomly generated data -* A set of randomly generated log files - -Create a new working directory where you want to download the files. From that directory, run the following commands: - -[source,shell] -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz - -Alternatively, for Windows users, run the following commands in Powershell: - -[source,shell] -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json -OutFile shakespeare.json -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip -OutFile accounts.zip -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz -OutFile logs.jsonl.gz - -Two of the data sets are compressed. To extract the files, use these commands: - -[source,shell] -unzip accounts.zip -gunzip logs.jsonl.gz - -[float] -==== Structure of the data sets - -The Shakespeare data set has the following structure: - -[source,json] -{ - "line_id": INT, - "play_name": "String", - "speech_number": INT, - "line_number": "String", - "speaker": "String", - "text_entry": "String", -} - -The accounts data set has the following structure: - -[source,json] -{ - "account_number": INT, - "balance": INT, - "firstname": "String", - "lastname": "String", - "age": INT, - "gender": "M or F", - "address": "String", - "employer": "String", - "email": "String", - "city": "String", - "state": "String" -} - -The logs data set has dozens of different fields. The notable fields include the following: - -[source,json] -{ - "memory": INT, - "geo.coordinates": "geo_point" - "@timestamp": "date" -} - -[float] -==== Set up mappings - -Before you load the Shakespeare and logs data sets, you must set up {ref}/mapping.html[_mappings_] for the fields. -Mappings divide the documents in the index into logical groups and specify the characteristics -of the fields. These characteristics include the searchability of the field -and whether it's _tokenized_, or broken up into separate words. - -NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. -You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {ref}/security-privileges.html[Security privileges] -for more information. - -Open the menu, then go to *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: - -[source,js] -PUT /shakespeare -{ - "mappings": { - "properties": { - "speaker": {"type": "keyword"}, - "play_name": {"type": "keyword"}, - "line_id": {"type": "integer"}, - "speech_number": {"type": "integer"} - } - } -} - -//CONSOLE - -The mapping specifies field characteristics for the data set: - -* The `speaker` and `play_name` fields are keyword fields. These fields are not analyzed. -The strings are treated as a single unit even if they contain multiple words. - -* The `line_id` and `speech_number` fields are integers. - -The logs data set requires a mapping to label the latitude and longitude pairs -as geographic locations by applying the `geo_point` type. - -[source,js] -PUT /logstash-2015.05.18 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.19 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.20 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -The accounts data set doesn't require any mappings. - -[float] -[[load-the-data-sets]] -==== Load the data sets - -At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] -API to load the data sets: - -[source,shell] -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/_bulk?pretty' --data-binary @accounts.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/shakespeare/_bulk?pretty' --data-binary @shakespeare.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/_bulk?pretty' --data-binary @logs.jsonl - -Or for Windows users, in Powershell: -[source,shell] -Invoke-RestMethod "http://:/bank/account/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "accounts.json" -Invoke-RestMethod "http://:/shakespeare/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "shakespeare.json" -Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "logs.jsonl" - -These commands might take some time to execute, depending on the available computing resources. - -When you define an index pattern, the indices that match the pattern must -exist in {es} and contain data. - -To verify the availability of the indices, open the menu, go to *Dev Tools > Console*, then enter: - -[source,js] -GET /_cat/indices?v - -Alternately, use: - -[source,shell] -`curl -XGET "http://localhost:9200/_cat/indices"`. - -The output should look similar to: - -[source,shell] -health status index pri rep docs.count docs.deleted store.size pri.store.size -yellow open bank 1 1 1000 0 418.2kb 418.2kb -yellow open shakespeare 1 1 111396 0 17.6mb 17.6mb -yellow open logstash-2015.05.18 1 1 4631 0 15.6mb 15.6mb -yellow open logstash-2015.05.19 1 1 4624 0 15.7mb 15.7mb -yellow open logstash-2015.05.20 1 1 4750 0 16.4mb 16.4mb diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc deleted file mode 100644 index 18ef862272f85..0000000000000 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ /dev/null @@ -1,159 +0,0 @@ -[[explore-kibana-using-sample-data]] -== Explore {kib} using sample data - -Ready to get some hands-on experience with {kib}? -In this tutorial, you’ll work with {kib} sample data and learn to: - -* <> - -* <> - -* <> - -NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. For more information, refer to -{ref}/security-privileges.html[Security privileges]. - -[float] -[[add-the-sample-data]] -=== Add the sample data - -Add the *Sample flight data*. - -. On the home page, click *Load a data set and a {kib} dashboard*. - -. On the *Sample flight data* card, click *Add data*. - -[float] -[[explore-the-data]] -=== Explore the data - -Explore the documents in the index that -match the selected index pattern. The index pattern tells {kib} which {es} index you want to -explore. - -. Open the menu, then go to *Discover*. - -. Make sure `kibana_sample_data_flights` is the current index pattern. -You might need to click *New* in the {kib} toolbar to refresh the data. -+ -You'll see a histogram that shows the distribution of -documents over time. A table lists the fields for -each document that matches the index. By default, all fields are shown. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-discover1.png[] - -. Hover over the list of *Available fields*, then click *Add* next -to each field you want explore in the table. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-discover2.png[] - -[float] -[[view-and-analyze-the-data]] -=== View and analyze the data - -A _dashboard_ is a collection of panels that provide you with an overview of your data that you can -use to analyze your data. Panels contain everything you need, including visualizations, -interactive controls, Markdown, and more. - -To open the *Global Flight* dashboard, open the menu, then go to *Dashboard*. - -[role="screenshot"] -image::getting-started/images/tutorial-sample-dashboard.png[] - -[float] -[[change-the-panel-data]] -==== Change the panel data - -To gain insights into your data, change the appearance and behavior of the panels. -For example, edit the metric panel to find the airline that has the lowest average fares. - -. In the {kib} toolbar, click *Edit*. - -. In the *Average Ticket Price* metric panel, open the panel menu, then select *Edit visualization*. - -. To change the data on the panel, use an {es} {ref}/search-aggregations.html[bucket aggregation], -which sorts the documents that match your search criteria into different categories or buckets. - -.. In the *Buckets* pane, select *Add > Split group*. - -.. From the *Aggregation* dropdown, select *Terms*. - -.. From the *Field* dropdown, select *Carrier*. - -.. Set *Descending* to *4*, then click *Update*. -+ -The average ticket price for all four airlines appear in the visualization builder. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit1.png[] - -. To save your changes, click *Save and return* in the {kib} toolbar. - -. To save the dashboard, click *Save* in the {kib} toolbar. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit2.png[] - -[float] -[[filter-and-query-the-data]] -==== Filter and query the data - -To focus in on the data you want to explore, use filters and queries. -For more information, refer to -{ref}/query-filter-context.html[Query and filter context]. - -To filter the data: - -. In the *Controls* visualization, select an *Origin City* and *Destination City*, then click *Apply changes*. -+ -The `OriginCityName` and the `DestCityName` fields filter the data in the panels. -+ -For example, the following dashboard shows the data for flights from London to Milan. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-filter.png[] - -. To manually add a filter, click *Add filter*, -then specify the data you want to view. - -. When you are finished experimenting, remove all filters. - -[[query-the-data]] -To query the data: - -. To view all flights out of Rome, enter the following in the *KQL* query bar, then click *Update*: -+ -[source,text] -OriginCityName: Rome - -. For a more complex query with AND and OR, enter: -+ -[source,text] -OriginCityName:Rome AND (Carrier:JetBeats OR Carrier:"Kibana Airlines") -+ -The dashboard panels update to display the flights out of Rome on JetBeats and -{kib} Airlines. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-query.png[] - -. When you are finished exploring, remove the query by -clearing the contents in the *KQL* query bar, then click *Update*. - -[float] -=== Next steps - -Now that you know the {kib} basics, try out the <> tutorial, where you'll learn to: - -* Add a data set to {kib} - -* Define an index pattern - -* Discover and explore data - -* Create and add panels to a dashboard - - diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc deleted file mode 100644 index a53c8cb6bc23d..0000000000000 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ /dev/null @@ -1,193 +0,0 @@ -[[tutorial-visualizing]] -=== Visualize your data - -Shape your data using a variety -of {kib} supported visualizations, tables, and more. In this tutorial, you'll create four -visualizations that you'll use to create a dashboard. - -To begin, open the menu, go to *Dashboard*, then click *Create new dashboard*. - -[float] -[[compare-the-number-of-speaking-parts-in-the-play]] -=== Compare the number of speaking parts in the plays - -To visualize the Shakespeare data and compare the number of speaking parts in the plays, create a bar chart using *Lens*. - -. Click *Create new*, then click *Lens* on the *New Visualization* window. -+ -[role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[Image showing different options for your new visualization] - -. Make sure the index pattern is *shakes*. - -. Display the play data along the x-axis. - -.. From the *Available fields* list, drag and drop *play_name* to the *X-axis* field. - -.. Click *Top values of play_name*. - -.. From the *Order direction* dropdown, select *Ascending*. - -.. In the *Label* field, enter `Play Name`. - -. Display the number of speaking parts per play along the y-axis. - -.. From the *Available fields* list, drag and drop *speaker* to the *Y-axis* field. - -.. Click *Unique count of speaker*. - -.. In the *Label* field, enter `Speaking Parts`. -+ -[role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[Bar chart showing the speaking parts data] - -. *Save* the chart with the name `Bar Example`. -+ -To show a tooltip with the number of speaking parts for that play, hover over a bar. -+ -Notice how the individual play names show up as whole phrases, instead of -broken up into individual words. This is the result of the mapping -you did at the beginning of the tutorial, when you marked the `play_name` field -as `not analyzed`. - -[float] -[[view-the-average-account-balance-by-age]] -=== View the average account balance by age - -To gain insight into the account balances in the bank account data, create a pie chart. In this tutorial, you'll use the {es} -{ref}/search-aggregations.html[bucket aggregation] to specify the pie slices to display. The bucket aggregation sorts the documents that match your search criteria into different -categories and establishes multiple ranges of account balances so that you can find how many accounts fall into each range. - -. Click *Create new*, then click *Pie* on the *New Visualization* window. - -. On the *Choose a source* window, select `ba*`. -+ -Since the default search matches all documents, the pie contains a single slice. - -. In the *Buckets* pane, click *Add > Split slices.* - -.. From the *Aggregation* dropdown, select *Range*. - -.. From the *Field* dropdown, select *balance*. - -.. Click *Add range* until there are six rows of fields, then define the following ranges: -+ -[source,text] -0 999 -1000 2999 -3000 6999 -7000 14999 -15000 30999 -31000 50000 - -. Click *Update*. -+ -The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. -+ -[role="screenshot"] -image::images/tutorial-visualize-pie-2.png[Pie chart displaying accounts that fall into each of the ranges, scaled to 1000 accounts] - -. Add another bucket aggregation that displays the ages of the account holders. - -.. In the *Buckets* pane, click *Add*, then click *Split slices*. - -.. From the *Sub aggregation* dropdown, select *Terms*. - -.. From the *Field* dropdown, select *age*, then click *Update*. -+ -The break down of the ages of the account holders are displayed -in a ring around the balance ranges. -+ -[role="screenshot"] -image::images/tutorial-visualize-pie-3.png[Final pie chart showing all of the changes] - -. Click *Save*, then enter `Pie Example` in the *Title* field. - -[float] -[role="xpack"] -[[visualize-geographic-information]] -=== Visualize geographic information - -To visualize geographic information in the log file data, use <>. - -. Click *Create new*, then click *Maps* on the *New Visualization* window. - -. To change the time, use the time filter. - -.. Set the *Start date* to `May 18, 2015 @ 12:00:00.000`. - -.. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. -+ -[role="screenshot"] -image::images/gs_maps_time_filter.png[Image showing the time filter for Maps tutorial] - -.. Click *Update* - -. Map the geo coordinates from the log files. - -.. Click *Add layer > Clusters and grids*. - -.. From the *Index pattern* dropdown, select *logstash*. - -.. Click *Add layer*. - -. Specify the *Layer Style*. - -.. From the *Fill color* dropdown, select the yellow to red color ramp. - -.. In the *Border width* field, enter `3`. - -.. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. -+ -[role="screenshot"] -image::images/tutorial-visualize-map-2.png[Example of a map visualization] - -. Click *Save*, then enter `Map Example` in the *Title* field. - -. Add the map to your dashboard. - -.. Open the menu, go to *Dashboard*, then click *Add*. - -.. On the *Add panels* flyout, click *Map Example*. - -[float] -[[tutorial-visualize-markdown]] -=== Add context to your visualizations with Markdown - -Add context to your new visualizations with Markdown text. - -. Click *Create new*, then click *Markdown* on the *New Visualization* window. - -. In the *Markdown* text field, enter: -+ -[source,markdown] -# This is a tutorial dashboard! -The Markdown widget uses **markdown** syntax. -> Blockquotes in Markdown use the > character. - -. Click *Update*. -+ -The Markdown renders in the preview pane. -+ -[role="screenshot"] -image::images/tutorial-visualize-md-2.png[Image showing example markdown editing field] - -. Click *Save*, then enter `Markdown Example` in the *Title* field. - -[role="screenshot"] -image::images/tutorial-dashboard.png[Final visualization with bar chart, pie chart, map, and markdown text field] - -[float] -=== Next steps - -Now that you have the basics, you're ready to start exploring your own system data with {kib}. - -* To add your own data to {kib}, refer to <>. - -* To search and filter your data, refer to {kibana-ref}/discover.html[Discover]. - -* To create a dashboard with your own data, refer to <>. - -* To create maps that you can add to your dashboards, refer to <>. - -* To create presentations of your live data, refer to <>. diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png deleted file mode 100644 index dd653bb046f45..0000000000000 Binary files a/docs/infrastructure/images/infra-sysmon.png and /dev/null differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc deleted file mode 100644 index 81a3022436a7e..0000000000000 --- a/docs/infrastructure/index.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-infra]] -= Metrics - -The {metrics-app} in {kib} enables you to monitor your infrastructure metrics and identify problems in real time. -You start with a visual summary of your infrastructure where you can view basic metrics for common servers, containers, and services. -Then you can drill down to view more detailed metrics or other information for that component. - -You can: - -* View your infrastructure metrics by hosts, Kubernetes pods, or Docker containers. -You can group and filter the data in various ways to help you identify the items that interest you. - -* View current and historic values for metrics such as CPU usage, memory usage, and network traffic for each component. -The available metrics depend on the kind of component being inspected. - -* Use *Metrics Explorer* to group and visualize multiple customizable metrics for one or more components in a graphical format. -You can optionally save these views and add them to {kibana-ref}/dashboard.html[dashboards]. - -* Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. - -* Create alerts based on metric thresholds for one or more components. - -[role="screenshot"] -image::infrastructure/images/infra-sysmon.png[Infrastructure Overview in Kibana] - -[float] -=== Get started - -To get started with Metrics, refer to {metrics-guide}/install-metrics-monitoring.html[Install Metrics]. - diff --git a/docs/ingest_manager/images/ingest-manager-start.png b/docs/ingest_manager/images/ingest-manager-start.png deleted file mode 100644 index 89174686a9768..0000000000000 Binary files a/docs/ingest_manager/images/ingest-manager-start.png and /dev/null differ diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png deleted file mode 100644 index ddd3346475da6..0000000000000 Binary files a/docs/logs/images/logs-console.png and /dev/null differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc deleted file mode 100644 index 45d4321f40556..0000000000000 --- a/docs/logs/index.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-logs]] -= Logs - -The Logs app in Kibana enables you to explore logs for common servers, containers, and services. - -The Logs app has a compact, console-like display that you can customize. -You can filter the logs by various fields, start and stop live streaming, and highlight text of interest. - -You can open the Logs app from the *Logs* tab in Kibana. -You can also open the Logs app directly from a component in the Metrics app. -In this case, you will only see the logs for the selected component. - -[role="screenshot"] -image::logs/images/logs-console.png[Logs Console in Kibana] - -[float] -=== Get started - -To get started with Elastic Logs, refer to {logs-guide}/install-logs-monitoring.html[Install Logs]. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 1bae04cc2e58b..8e8d0e5bf996e 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -454,9 +454,6 @@ The opacity of the chart items that are dimmed when highlighting another element of the chart. The lower this number, the more the highlighted element stands out. This must be a number between 0 and 1. -[[visualization-loadingdelay]]`visualization:loadingDelay`:: -The time to wait before dimming visualizations during a query. - [[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc index 73cf40c4d7c40..f348812550978 100644 --- a/docs/management/alerting/alert-management.asciidoc +++ b/docs/management/alerting/alert-management.asciidoc @@ -4,7 +4,7 @@ beta[] -The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: +The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: * <> alerts * <> including enabling/disabling, muting/unmuting, and deleting @@ -39,7 +39,7 @@ image::images/alerts-filter-by-action-type.png[Filtering the alert list by type [[create-edit-alerts]] ==== Creating and editing alerts -Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. diff --git a/docs/management/images/index-lifecycle-policies-create.png b/docs/management/images/index-lifecycle-policies-create.png deleted file mode 100644 index f6d86fa9b4ea5..0000000000000 Binary files a/docs/management/images/index-lifecycle-policies-create.png and /dev/null differ diff --git a/docs/management/images/index_lifecycle_policies_options.png b/docs/management/images/index_lifecycle_policies_options.png deleted file mode 100644 index 184188be181e8..0000000000000 Binary files a/docs/management/images/index_lifecycle_policies_options.png and /dev/null differ diff --git a/docs/management/images/index_management_add_policy.png b/docs/management/images/index_management_add_policy.png deleted file mode 100644 index f0fe493a2e491..0000000000000 Binary files a/docs/management/images/index_management_add_policy.png and /dev/null differ diff --git a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc b/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc deleted file mode 100644 index 0fec62d895754..0000000000000 --- a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc +++ /dev/null @@ -1,17 +0,0 @@ -[role="xpack"] -[[adding-policy-to-index]] -=== Adding a policy to an index - -To add a lifecycle policy to an index and view the status for indices -managed by a policy, open the menu, then go to *Stack Management > Data > Index Management*. -This page lists your -{es} indices, which you can filter by lifecycle status and lifecycle phase. - -To add a policy, select the index name and then select *Manage Index > Add lifecycle policy*. -You’ll see the policy name, the phase the index is in, the current -action, and if any errors occurred performing that action. - -To remove a policy from an index, select *Manage Index > Remove lifecycle policy*. - -[role="screenshot"] -image::images/index_management_add_policy.png[][UI for adding a policy to an index] diff --git a/docs/management/index-lifecycle-policies/create-policy.asciidoc b/docs/management/index-lifecycle-policies/create-policy.asciidoc deleted file mode 100644 index 7849ef6b92054..0000000000000 --- a/docs/management/index-lifecycle-policies/create-policy.asciidoc +++ /dev/null @@ -1,93 +0,0 @@ -[role="xpack"] -[[creating-index-lifecycle-policies]] -=== Creating an index lifecycle policy - -An index lifecycle policy enables you to define rules over when to perform -certain actions, such as a rollover or force merge, on an index. Index lifecycle -management automates execution of those actions at the right time. - -When you create an index lifecycle policy, consider the tradeoffs between -performance and availability. As you move your index through the lifecycle, -you’re likely moving your data to less performant hardware and reducing the -number of shards and replicas. It’s important to ensure that the index -continues to have enough replicas to prevent data loss in the event of failures. - -*Index Lifecycle Policies* is automatically enabled in {kib}. Open the menu, then go to -*Stack Management > {es} > Index Lifecycle Policies*. - -NOTE: If you don’t want to use this feature, you can disable it by setting -`xpack.ilm.enabled` to false in your `kibana.yml` configuration file. If you -disable *Index Management*, then *Index Lifecycle Policies* is also disabled. - -[role="screenshot"] -image::images/index-lifecycle-policies-create.png[][UI for creating an index lifecycle policy] - -==== Defining the phases of the index lifecycle - -You can define up to four phases in the index lifecycle. For each phase, you -can enable actions to optimize performance for that phase. - -The four phases in the index lifecycle are: - -* *Hot.* The index is actively being queried and written to. You can -roll over to a new index when the -original index reaches a specified size, document count, or age. When a rollover occurs, a new -index is created, added to the index alias, and designated as the new “hot” -index. You can still query the previous indices, but you only ever write to -the “hot” index. See <>. - -* *Warm.* The index is typically searched at a lower rate than when the data is -hot. The index is not used for storing new data, but might occasionally add -late-arriving data, for example, from a Beat with a network problem that's now fixed. -You can optionally shrink the number replicas and move the shards to a -different set of nodes with smaller or less performant hardware. You can also -reduce the number of primary shards and force merge the index into -smaller {ref}/indices-segments.html[segments]. - -* *Cold.* The index is no longer being updated and is seldom queried, but is -still searchable. If you have a big deployment, you can move it to even -less performant hardware. You might also reduce the number of replicas because -you expect the data to be queried less frequently. To keep the index searchable -for a longer period, and reduce the hardware requirements, you can use the -{ref}/frozen-indices.html[freeze action]. Queries are slower on a frozen index because the index is -reloaded from the disk to RAM on demand. - -* *Delete.* The index is no longer relevant. You can define when it is safe to -delete it. - -The index lifecycle always includes an active hot phase. The warm, cold, and -delete phases are optional. For example, you might define all four phases for -one policy and only a hot and delete phase for another. See {ref}/_actions.html[Actions] -for more information on the actions available in each phase. - -[[setting-a-rollover-action]] -==== Setting a rollover action - -The {ref}/indices-rollover-index.html[rollover] action enables you to automatically roll over to a new index based -on the index size, document count, or age. Rolling over to a new index based on -these criteria is preferable to time-based rollovers. Rolling over at an arbitrary -time often results in many small indices, which can have a negative impact on performance and resource usage. - -When you create an index lifecycle policy, the rollover action is enabled -by default. The default size for triggering the rollover is 50 gigabytes, and -the default age is 30 days. The rollover occurs when any of the criteria are met. - -With the rollover action enabled, you can move to the warm phase on rollover or you can -time the move for a specified number of hours or days after the rollover. The -move to the cold and delete phases is based on the time from the rollover. - -If you are using daily indices (created by Logstash or another client) and you -want to use the index lifecycle policy to manage aging data, you can -disable the rollover action in the hot phase. You can then -transition to the warm, cold, and delete phases based on the time of index creation. - -==== Setting the index priority - -For the hot, warm, and cold phases, you can set a priority for recovering -indices after a node restart. Indices with higher priorities are recovered -before indices with lower priorities. By default, the index priority is set to -100 in the hot phase, 50 in the warm phase, and 0 in the cold phase. -If the cold phase of one index has data that -is more important than the data in the hot phase of another, you might increase -the index priority in the cold phase. See -{ref}/recovery-prioritization.html[Index recovery prioritization]. diff --git a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc b/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc deleted file mode 100644 index ba1d79710de05..0000000000000 --- a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[role="xpack"] -[[index-lifecycle-policies]] -== Index Lifecycle Policies - -If you're working with time series data, you don't want to continually dump -everything into a single index. Instead, you might periodically roll over the -data to a new index to keep it from growing so big it's slow and expensive. -As the index ages and you query it less frequently, you’ll likely move it to -less expensive hardware and reduce the number of shards and replicas. - -To automatically move an index through its lifecycle, you can create a policy -to define actions to perform on the index as it ages. Index lifecycle policies -are especially useful when working with {beats-ref}/beats-reference.html[Beats] -data shippers, which continually -send operational data, such as metrics and logs, to Elasticsearch. You can -automate a rollover to a new index when the existing index reaches a specified -size or age. This ensures that all indices have a similar size instead of having -daily indices where size can vary based on the number of Beats and the number -of events sent. - -{kib}’s *Index Lifecycle Policies* walks you through the process for creating -and configuring a policy. Before using this feature, you should be familiar -with index lifecycle management: - -* For an introduction, refer to -{ref}/getting-started-index-lifecycle-management.html[Getting started with index -lifecycle management]. -* To dig into the concepts and technical details, see -{ref}/index-lifecycle-management.html[Managing the index lifecycle]. -* To check out the APIs, see {ref}/index-lifecycle-management-api.html[Index lifecycle management API]. diff --git a/docs/management/index-lifecycle-policies/manage-policy.asciidoc b/docs/management/index-lifecycle-policies/manage-policy.asciidoc deleted file mode 100644 index 8e2dc96de4b99..0000000000000 --- a/docs/management/index-lifecycle-policies/manage-policy.asciidoc +++ /dev/null @@ -1,34 +0,0 @@ -[role="xpack"] -[[managing-index-lifecycle-policies]] -=== Managing index lifecycle policies - -Your configured policies appear on the *Index lifecycle policies* page. -You can update an existing index lifecycle policy to fix errors or change -strategies for newly created indices. To edit a policy, select its name. - -[role="screenshot"] -image::images/index_lifecycle_policies_options.png[][UI for viewing and editing an index lifecycle policy] - -In addition, you can: - -* *View indices linked to the policy.* This is important when editing a policy. -Any changes you make affect all indices attached to the policy. The settings -for the current phase are cached, so the update doesn’t affect that phase. This -prevents conflicts when you’re modifying a phase that is currently executing on -an index. The changes takes effect when the next phase in the index lifecycle begins. - -* *Add the policy to an index template.* When an index is automatically -created using the index template, the policy is applied. If the index is rolled -over, the policies for any matching index templates are applied to the newly -created index. For more information, see {ref}/indices-templates.html[Index templates]. - -* *Delete a policy.* You can’t delete a policy that is currently in use or -recover a deleted index. - -[float] -=== Required permissions - -The `manage_ilm` cluster privilege is required to access *Index lifecycle policies*. - -You can add these privileges in *Stack Management > Security > Roles*. - diff --git a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png old mode 100755 new mode 100644 index 8d8b8aa4b42e3..2de7449affd0c Binary files a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png and b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png differ diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc index da2d3b8accac2..7986e4e56279a 100644 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc @@ -62,11 +62,40 @@ You also want to know where the request is coming from. . In *Ingest Node Pipelines*, click *Create a pipeline*. . Provide a name and description for the pipeline. -. Define the processors: +. Add a grok processor to parse the log message: + +.. Click *Add a processor* and select the *Grok* processor type. +.. Set the field input to `message` and enter the following grok pattern: + [source,js] ---------------------------------- -[ +%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent} +---------------------------------- ++ +.. Click *Update* to save the processor. + +. Add processors to map the date, IP, and user agent fields. + +.. Map the appropriate field to each processor type: ++ +-- +* **Date**: `timestamp` +* **GeoIP**: `clientip` +* **User agent**: `agent` + +For the **Date** processor, you also need to specify the date format you want to use: `dd/MMM/YYYY:HH:mm:ss Z`. +-- +Your form should look similar to this: ++ +[role="screenshot"] +image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] ++ +Alternatively, you can click the **Import processors** link and define the processors as JSON: ++ +[source,js] +---------------------------------- +{ + "processors": [ { "grok": { "field": "message", @@ -90,19 +119,16 @@ You also want to know where the request is coming from. } } ] +} ---------------------------------- + -This code defines four {ref}/ingest-processors.html[processors] that run sequentially: +The four {ref}/ingest-processors.html[processors] will run sequentially: {ref}/grok-processor.html[grok], {ref}/date-processor.html[date], -{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. -Your form should look similar to this: -+ -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] +{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. You can reorder processors using the arrow icon next to each processor. -. To verify that the pipeline gives the expected outcome, click *Test pipeline*. +. To test the pipeline to verify that it produces the expected results, click *Add documents*. -. In the *Document* tab, provide the following sample document for testing: +. In the *Documents* tab, provide a sample document for testing: + [source,js] ---------------------------------- diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index e20f384b5ed18..7324f45594bd7 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -67,7 +67,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. === Try it: Create and visualize rolled up data This example creates a rollup job to capture log data from sample web logs. -To follow along, add the <>. +To follow along, add the sample web logs data set. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` to roll up once a day into the index `rollup_logstash`. You’ll bucket the diff --git a/docs/observability/images/apm-app.png b/docs/observability/images/apm-app.png new file mode 100644 index 0000000000000..acbaa70c7f2f1 Binary files /dev/null and b/docs/observability/images/apm-app.png differ diff --git a/docs/observability/images/logs-app.png b/docs/observability/images/logs-app.png new file mode 100644 index 0000000000000..1138ec175e5bf Binary files /dev/null and b/docs/observability/images/logs-app.png differ diff --git a/docs/observability/images/metrics-app.png b/docs/observability/images/metrics-app.png new file mode 100644 index 0000000000000..8c00a31974a70 Binary files /dev/null and b/docs/observability/images/metrics-app.png differ diff --git a/docs/observability/images/uptime-app.png b/docs/observability/images/uptime-app.png new file mode 100644 index 0000000000000..522a696adf506 Binary files /dev/null and b/docs/observability/images/uptime-app.png differ diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc index d63402e8df2fb..c924cea3712dd 100644 --- a/docs/observability/index.asciidoc +++ b/docs/observability/index.asciidoc @@ -13,12 +13,69 @@ With *Observability*, you have: * *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. * An alerts chart to keep you informed of any issues that you may need to resolve quickly. +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. + [role="screenshot"] image::observability/images/observability-overview.png[Observability Overview in {kib}] [float] -== Get started +[[logs-app]] +== Logs -{kib} provides step-by-step instructions to help you add and configure your data -sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information -and instructions. +The {logs-app} in {kib} enables you to search, filter, and tail all your logs +ingested into {es}. Instead of having to log into different servers, change +directories, and tail individual files, all your logs are available in the {logs-app}. + +There is live streaming of logs, filtering using auto-complete, and a logs histogram +for quick navigation. You can also use machine learning to detect specific log +anomalies automatically and categorize log messages to quickly identify patterns in your +log events. + +To get started with the {logs-app}, see {observability-guide}/ingest-logs.html[Ingest logs]. + +[role="screenshot"] +image::observability/images/logs-app.png[Logs app in {kib}] + +[float] +[[metrics-app]] +== Metrics + +The {metrics-app} in {kib} enables you to visualize infrastructure metrics +to help diagnose problematic spikes, identify high resource utilization, +automatically discover and track pods, and unify your metrics +with logs and APM data in {es}. + +To get started with the {metrics-app}, see {observability-guide}/ingest-metrics.html[Ingest metrics]. + +[role="screenshot"] +image::observability/images/metrics-app.png[Metrics app in {kib}] + +[float] +[[uptime-app]] +== Uptime + +The {uptime-app} in {kib} enables you to monitor the availability and response times +of applications and services in real time, and detect problems before they affect users. +You can monitor the status of network endpoints via HTTP/S, TCP, and ICMP, explore +endpoint status over time, drill down into specific monitors, and view a high-level +snapshot of your environment at any point in time. + +To get started with the {uptime-app}, see {observability-guide}/ingest-uptime.html[Ingest uptime data]. + +[role="screenshot"] +image::observability/images/uptime-app.png[Uptime app in {kib}] + +[float] +[[apm-app]] +== APM + +The APM app in {kib} enables you to monitors software services and applications in real time, +collect unhandled errors and exceptions, and automatically pick up basic host-level metrics +and agent specific metrics. + +To get started with the APM app, see <>. + +[role="screenshot"] +image::observability/images/apm-app.png[APM app in {kib}] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 5067bc08bec99..d8c200450d7e5 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -59,7 +59,7 @@ This page has moved. Please see <>. [role="exclude",id="add-sample-data"] == Add sample data -This page has moved. Please see <>. +This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map @@ -112,3 +112,34 @@ This content has moved. See This content has moved. See {ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Connect to a remote cluster]. + +[role="exclude",id="adding-policy-to-index"] +== Adding a policy to an index + +This content has moved. See +{ref}/set-up-lifecycle-policy.html[Configure a lifecycle policy]. + +[role="exclude",id="creating-index-lifecycle-policies"] +== Creating an index lifecycle policy + +This content has moved. See +{ref}/set-up-lifecycle-policy.html[Configure a lifecycle policy]. + +[role="exclude",id="index-lifecycle-policies"] +== Index Lifecycle Policies + +This content has moved. See +{ref}/index-lifecycle-management.html[ILM: Manage the index lifecycle]. + +[role="exclude",id="managing-index-lifecycle-policies"] +== Managing index lifecycle policies + +This content has moved. See +{ref}/index-lifecycle-management.html[ILM: Manage the index lifecycle]. + +[role="exclude",id="tutorial-define-index"] +== Define your index patterns + +This content has moved. See +<>. + diff --git a/docs/settings/ingest-manager-settings.asciidoc b/docs/settings/fleet-settings.asciidoc similarity index 68% rename from docs/settings/ingest-manager-settings.asciidoc rename to docs/settings/fleet-settings.asciidoc index 9fa83fc242b4a..9c28d28003175 100644 --- a/docs/settings/ingest-manager-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -1,29 +1,29 @@ [role="xpack"] -[[ingest-manager-settings-kb]] -=== {ingest-manager} settings in {kib} +[[fleet-settings-kb]] +=== {fleet} settings in {kib} ++++ -{ingest-manager} settings +{fleet} settings ++++ experimental[] You can configure `xpack.fleet` settings in your `kibana.yml`. -By default, {ingest-manager} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. +By default, {fleet} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. See the {ingest-guide}/index.html[Ingest Management] docs for more information. -[[general-ingest-manager-settings-kb]] -==== General {ingest-manager} settings +[[general-fleet-settings-kb]] +==== General {fleet} settings [cols="2*<"] |=== | `xpack.fleet.enabled` {ess-icon} - | Set to `true` (default) to enable {ingest-manager}. + | Set to `true` (default) to enable {fleet}. | `xpack.fleet.agents.enabled` {ess-icon} | Set to `true` (default) to enable {fleet}. |=== -[[ingest-manager-data-visualizer-settings]] +[[fleet-data-visualizer-settings]] ==== {package-manager} settings diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 04fed0d6204b7..9d9cc92401896 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -20,4 +20,4 @@ include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] include::i18n-settings.asciidoc[] -include::ingest-manager-settings.asciidoc[] +include::fleet-settings.asciidoc[] diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index ea02afb8a9fda..0daa3f1e0e55e 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -11,7 +11,7 @@ To start working with your data in {kib}, you can: * Connect {kib} with existing {es} indices. -If you're not ready to use your own data, you can add a <> +If you're not ready to use your own data, you can add a <> to see all that you can do in {kib}. [float] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 903bb59cef380..2f2c87ca9c7d4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -638,7 +638,7 @@ include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] -include::{kib-repo-dir}/settings/ingest-manager-settings.asciidoc[] +include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] diff --git a/docs/uptime/images/uptime-overview.png b/docs/uptime/images/uptime-overview.png deleted file mode 100644 index 25c88b2d14287..0000000000000 Binary files a/docs/uptime/images/uptime-overview.png and /dev/null differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc deleted file mode 100644 index 66c9e9357420f..0000000000000 --- a/docs/uptime/index.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-uptime]] -= Uptime - -The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. -You can explore endpoint status over time, drill down into specific monitors, -and view a high-level snapshot of your environment at any point in time. - -[role="screenshot"] -image::images/uptime-overview.png[Uptime app overview] - -[float] -=== Get started - -To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[Install Uptime]. - - - diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 4a99c70f9d961..f71e43c5defc7 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,7 +2,7 @@ [[alert-types]] == Alert types -{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index bdb72b1658cd2..f8656b87cbe04 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. image::images/alerting-overview.png[Alerts and actions UI] @@ -148,7 +148,7 @@ Functionally, {kib} alerting differs in that: * {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. * Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. [float] @@ -170,9 +170,9 @@ If you are using an *on-premises* Elastic Stack deployment with <> -* <> +* <> * <> -* <> +* <> See <> for more information on configuring roles that provide access to these features. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 7f201d2c39e89..89a487ca8fb32 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 0b0eb7a318495..297dfac5b10bd 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -17,6 +17,8 @@ With Canvas, you can: * Focus the data you want to display with filters. +To begin, open the menu, then go to *Canvas*. + [role="screenshot"] image::images/canvas-gs-example.png[Getting started example] @@ -26,7 +28,8 @@ For a quick overview of Canvas, watch link:https://www.youtube.com/watch?v=ZqvF_ [[create-workpads]] == Create workpads -A _workpad_ provides you with a space where you can build presentations of your live data. +A _workpad_ provides you with a space where you can build presentations of your live data. With Canvas, +you can create a workpad from scratch, start with a preconfigured workpad, import an existing workpad, or use a sample data workpad. [float] [[start-with-a-blank-workpad]] @@ -34,19 +37,15 @@ A _workpad_ provides you with a space where you can build presentations of your To use the background colors, images, and data of your choice, start with a blank workpad. -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, click *Create workpad*. +. On the *Canvas workpads* page, click *Create workpad*. -. Add a *Name* to your workpad. +. Specify the *Workpad settings*. -. In the *Width* and *Height* fields, specify the size. +.. Add a *Name* to your workpad. -. Select the layout. -+ -For example, click *720p* for a traditional presentation layout. +.. In the *Width* and *Height* fields, specify the size, or select one of default layouts. -. Click the *Background color* picker, then select the background color for your workpad. +.. Click the *Background* color picker, then select the color for your workpad. + [role="screenshot"] image::images/canvas-background-color-picker.png[Canvas color picker] @@ -57,9 +56,7 @@ image::images/canvas-background-color-picker.png[Canvas color picker] If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, select *Templates*. +. On the *Canvas workpads* page, select *Templates*. . Click the preconfigured template that you want to use. @@ -69,17 +66,15 @@ If you're unsure about where to start, you can use one of the preconfigured temp [[import-existing-workpads]] === Import existing workpads -When you want to use a workpad that someone else has already started, import the JSON file into Canvas. - -. Open the menu, then go to *Canvas*. +When you want to use a workpad that someone else has already started, import the JSON file. -. On the *Canvas workpads* view, click and drag the file to the *Import workpad JSON file* field. +To begin, drag the file to the *Import workpad JSON file* field on the *Canvas workpads* page. [float] [[use-sample-data-workpads]] === Use sample data workpads -Each of the sample data sets comes with a Canvas workpad that you can use for your own workpad inspiration. +Each of the {kib} sample data sets comes with a workpad that you can use for your own workpad inspiration. . Add a {kibana-ref}/add-sample-data.html[sample data set]. @@ -123,12 +118,12 @@ To save a group of elements, press and hold Shift, select the elements you want Elements are saved in *Add element > My elements*. [float] -[[add-existing-visuualizations]] -=== Add existing visualizations +[[add-saved-objects]] +=== Add saved objects Add <> to your workpad, such as maps and visualizations. -. Click *Add element > Add from Visualize Library*. +. Click *Add element > Add from {kib}*. . Select the saved object you want to add. + diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc index e50c1281beede..5e928fd731bb4 100644 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -39,7 +39,7 @@ Create the *Host Overview* drilldown shown above. *Set up the dashboards* -. Add the <> data set. +. Add the sample web logs data set. . Create a new dashboard, called `Host Overview`, and include these visualizations from the sample data set: diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 620a2d2056bf1..b71dfb016c765 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -36,7 +36,7 @@ The following panels support URL drilldowns: This example shows how to create the "Show on Github" drilldown shown above. -. Add the <> data set. +. Add the sample web logs data set. . Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but it should work for demonstration purposes. . In the dashboard menu bar, click *Edit*. . In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 6fd30690b988e..378f7a53a6650 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -64,16 +64,17 @@ Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) va [[vega-queries]] ==== Writing {es} queries in Vega -experimental[] {kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements -with support for direct {es} queries specified as a `url`. +{kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements +with support for direct {es} queries specified as `url`. -Because of this, {kib} is **unable to support dynamically loaded data**, +{kib} is **unable to support dynamically loaded data**, which would otherwise work in Vega. All data is fetched before it's passed to the Vega renderer. -To define an {es} query in Vega, set the `url` to an object. {kib} will parse +To define an {es} query in Vega, set the `url` to an object. {kib} parses the object looking for special tokens that allow your query to integrate with {kib}. -These tokens are: + +Tokens include the following: * `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard * `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter @@ -87,8 +88,7 @@ These tokens are: * `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters * `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters -Putting this together, an example query that counts the number of documents in -a specific index: +For example, the following query counts the number of documents in a specific index: [source,yaml] ---- diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc deleted file mode 100644 index a877f6a66a79a..0000000000000 --- a/docs/user/getting-started.asciidoc +++ /dev/null @@ -1,61 +0,0 @@ -[[get-started]] -= Get started - -[partintro] --- - -Ready to try out {kib} and see what it can do? The quickest way to get started with {kib} is to set up on Cloud, then add a sample data set to explore the full range of {kib} features. - -[float] -[[set-up-on-cloud]] -== Set up on cloud - -include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] - -[float] -[[gs-get-data-into-kibana]] -== Get data into {kib} - -The easiest way to get data into {kib} is to add a sample data set. - -{kib} has several sample data sets that you can use before loading your own data: - -* *Sample eCommerce orders* includes visualizations for tracking product-related information, -such as cost, revenue, and price. - -* *Sample flight data* includes visualizations for monitoring flight routes. - -* *Sample web logs* includes visualizations for monitoring website traffic. - -To use the sample data sets: - -. Go to the home page. - -. Click *Load a data set and a {kib} dashboard*. - -. Click *View data* and view the prepackaged dashboards, maps, and more. - -[role="screenshot"] -image::getting-started/images/add-sample-data.png[] - -NOTE: The timestamps in the sample data sets are relative to when they are installed. -If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. - -[float] -== Next steps - -* To get a hands-on experience creating visualizations, follow the <> tutorial. - -* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. - --- - -include::{kib-repo-dir}/getting-started/tutorial-sample-data.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-full-experience.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-define-index.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index e909626c5779c..d375b6f425e54 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -2,6 +2,8 @@ include::introduction.asciidoc[] include::whats-new.asciidoc[] +include::{kib-repo-dir}/getting-started/quick-start-guide.asciidoc[] + include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] @@ -11,8 +13,6 @@ include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] -include::getting-started.asciidoc[] - include::discover.asciidoc[] include::dashboard/dashboard.asciidoc[] @@ -27,14 +27,8 @@ include::graph/index.asciidoc[] include::{kib-repo-dir}/observability/index.asciidoc[] -include::{kib-repo-dir}/logs/index.asciidoc[] - -include::{kib-repo-dir}/infrastructure/index.asciidoc[] - include::{kib-repo-dir}/apm/index.asciidoc[] -include::{kib-repo-dir}/uptime/index.asciidoc[] - include::{kib-repo-dir}/siem/index.asciidoc[] include::dev-tools.asciidoc[] @@ -43,7 +37,7 @@ include::monitoring/index.asciidoc[] include::management.asciidoc[] -include::{kib-repo-dir}/ingest_manager/ingest-manager.asciidoc[] +include::{kib-repo-dir}/fleet/fleet.asciidoc[] include::reporting/index.asciidoc[] diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 079d183dd959d..7e5dc59b03a2c 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -155,6 +155,6 @@ and start exploring data in minutes. You can also <> — no code, no additional infrastructure required. -Our <> and in-product guidance can +Our <> and in-product guidance can help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] in the top navigation bar for help with questions or to provide feedback. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e0d550a15a907..c371aa695c475 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -40,7 +40,7 @@ a| <> flushing, and clearing the cache. Practicing good index management ensures that your data is stored cost effectively. -| <> +| {ref}/index-lifecycle-management.html[Index Lifecycle Policies] |Create a policy for defining the lifecycle of an index as it ages through the hot, warm, cold, and delete phases. Such policies help you control operation costs @@ -180,14 +180,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/manage-policy.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index.asciidoc[] - include::{kib-repo-dir}/management/managing-indices.asciidoc[] include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index cc4af9041bcd9..bf7be6284b1a9 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -28,7 +28,7 @@ To complete this tutorial, you'll need the following: * **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or +* **Data**: You can use <> or live data. In the following steps, Filebeat and Metricbeat data are used. [float] diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 223b8c55a5fde..6025d24665901 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions", "dashboard", "savedObjects"], + "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index 65ec22a2759e2..8d633a892ec6d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -26,10 +26,10 @@ import { EmbeddableOutput, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, + AttributeService, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; -import { AttributeService } from '../../../../src/plugins/dashboard/public'; export const BOOK_EMBEDDABLE = 'book'; export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index a535552282150..f569e2e8d154d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -25,6 +25,8 @@ import { EmbeddableFactoryDefinition, IContainer, EmbeddableFactory, + EmbeddableStart, + AttributeService, } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, @@ -38,11 +40,10 @@ import { SavedObjectsClientContract, SimpleSavedObject, } from '../../../../src/core/public'; -import { DashboardStart, AttributeService } from '../../../../src/plugins/dashboard/public'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; interface StartServices { - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; openModal: OverlayStart['openModal']; savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart; diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 77035b6887734..e2133a8d51ea2 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -22,7 +22,11 @@ import { i18n } from '@kbn/i18n'; import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { createAction } from '../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public'; +import { + ViewMode, + SavedObjectEmbeddableInput, + EmbeddableStart, +} from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, BOOK_EMBEDDABLE, @@ -30,13 +34,12 @@ import { BookByValueInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; -import { DashboardStart } from '../../../../src/plugins/dashboard/public'; import { OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { SavedObjectsClientContract } from '../../../../src/core/target/types/public/saved_objects'; interface StartServices { openModal: OverlayStart['openModal']; - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; savedObjectsClient: SavedObjectsClientContract; } diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 6d1b119e741bd..9b9770e40611e 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -62,7 +62,6 @@ import { ACTION_ADD_BOOK_TO_LIBRARY, createAddBookToLibraryAction, } from './book/add_book_to_library_action'; -import { DashboardStart } from '../../../src/plugins/dashboard/public'; import { ACTION_UNLINK_BOOK_FROM_LIBRARY, createUnlinkBookFromLibraryAction, @@ -75,7 +74,6 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; - dashboard: DashboardStart; savedObjectsClient: SavedObjectsClient; } @@ -157,7 +155,7 @@ export class EmbeddableExamplesPlugin this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( BOOK_EMBEDDABLE, new BookEmbeddableFactoryDefinition(async () => ({ - getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, overlays: (await core.getStartServices())[0].overlays, @@ -165,7 +163,7 @@ export class EmbeddableExamplesPlugin ); const editBookAction = createEditBookAction(async () => ({ - getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, })); diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 61e50d3379d03..2425f3bbad8a9 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -232,7 +232,6 @@ export const SearchExamplesApp = ({ Index Pattern => { const es = data.search.getSearchStrategy('es'); return { - search: async (context, request, options): Promise => { - const esSearchRes = await es.search(context, request, options); - return { - ...esSearchRes, - cool: request.get_cool ? 'YES' : 'NOPE', - }; - }, + search: (request, options, context) => + es.search(request, options, context).pipe( + map((esSearchRes) => ({ + ...esSearchRes, + cool: request.get_cool ? 'YES' : 'NOPE', + })) + ), cancel: async (context, id) => { if (es.cancel) { es.cancel(context, id); diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts index 6eb21cf34b4a3..21ae38b99f3d2 100644 --- a/examples/search_examples/server/routes/server_search_route.ts +++ b/examples/search_examples/server/routes/server_search_route.ts @@ -39,26 +39,28 @@ export function registerServerSearchRoute(router: IRouter, data: DataPluginStart // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion. // If you wish to run the search with polling (in basic+), you'd have to poll on the search API. // Please reach out to the @app-arch-team if you need this to be implemented. - const res = await data.search.search( - context, - { - params: { - index, - body: { - aggs: { - '1': { - avg: { - field, + const res = await data.search + .search( + { + params: { + index, + body: { + aggs: { + '1': { + avg: { + field, + }, }, }, }, + waitForCompletionTimeout: '5m', + keepAlive: '5m', }, - waitForCompletionTimeout: '5m', - keepAlive: '5m', - }, - } as IEsSearchRequest, - {} - ); + } as IEsSearchRequest, + {}, + context + ) + .toPromise(); return response.ok({ body: { diff --git a/package.json b/package.json index 9f9ad9ead7096..732ee1fd3038b 100644 --- a/package.json +++ b/package.json @@ -364,7 +364,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^84.0.0", + "chromedriver": "^86.0.0", "classnames": "2.2.6", "compare-versions": "3.5.1", "d3": "3.5.17", @@ -480,7 +480,7 @@ "typescript": "4.0.2", "ui-select": "0.19.8", "vega": "^5.17.0", - "vega-lite": "^4.16.8", + "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.24.2", "vinyl-fs": "^3.0.3", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md index c7b98224c4e57..12fc33dfaffb0 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md @@ -8,10 +8,23 @@ This class integrates with the `ciStats.trackBuild {}` Jenkins Pipeline function To create an instance of the reporter, import the class and call `CiStatsReporter.fromEnv(log)` (passing it a tooling log). -#### `CiStatsReporter#metrics(metrics: Array<{ group: string, id: string, value: number }>)` +#### `CiStatsReporter#metrics(metrics: Metric[])` Use this method to record metrics in the Kibana CI Stats service. +```ts +interface Metric { + group: string, + id: string, + value: number, + // optional limit, values which exceed the limit will fail PRs + limit?: number + // optional path, relative to the root of the repo, where config values + // are defined. Will be linked to in PRs which have overages. + limitConfigPath?: string +} +``` + Example: ```ts diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index b0378ab6c5cd5..a2f3b63daec50 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -29,7 +29,13 @@ interface Config { buildId: string; } -export type CiStatsMetrics = Array<{ group: string; id: string; value: number }>; +export type CiStatsMetrics = Array<{ + group: string; + id: string; + value: number; + limit?: number; + limitConfigPath?: string; +}>; function parseConfig(log: ToolingLog) { const configJson = process.env.KIBANA_CI_STATS_CONFIG; diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md index a666907f02678..3fdf915e84c21 100644 --- a/packages/kbn-optimizer/README.md +++ b/packages/kbn-optimizer/README.md @@ -84,9 +84,9 @@ const config = OptimizerConfig.create({ dist: true }); -await runOptimizer(config) - .pipe(logOptimizerState(log, config)) - .toPromise(); +await lastValueFrom( + runOptimizer(config).pipe(logOptimizerState(log, config)) +); ``` This is essentially what we're doing in [`script/build_kibana_platform_plugins`][Cli] and the new [build system task][BuildTask]. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml new file mode 100644 index 0000000000000..b075a678bff38 --- /dev/null +++ b/packages/kbn-optimizer/limits.yml @@ -0,0 +1,100 @@ +pageLoadAssetSize: + advancedSettings: 27596 + alerts: 106936 + apm: 64385 + apmOss: 18996 + beatsManagement: 188135 + bfetch: 41874 + canvas: 1066647 + charts: 159211 + cloud: 21076 + console: 46091 + core: 692106 + crossClusterReplication: 65408 + dashboard: 374194 + dashboardEnhanced: 65646 + dashboardMode: 22716 + data: 1170713 + dataEnhanced: 50420 + devTools: 38637 + discover: 105145 + discoverEnhanced: 42730 + embeddable: 312874 + embeddableEnhanced: 41145 + enterpriseSearch: 35741 + esUiShared: 326654 + expressions: 224136 + features: 31211 + fileUpload: 24717 + globalSearch: 43548 + globalSearchBar: 62888 + globalSearchProviders: 25554 + graph: 31504 + grokdebugger: 26779 + home: 41661 + indexLifecycleManagement: 107090 + indexManagement: 140608 + indexPatternManagement: 154222 + infra: 197873 + ingestManager: 415829 + ingestPipelines: 58003 + inputControlVis: 172675 + inspector: 148711 + kibanaLegacy: 107711 + kibanaOverview: 56279 + kibanaReact: 161921 + kibanaUtils: 198829 + lens: 96624 + licenseManagement: 41817 + licensing: 39008 + lists: 183665 + logstash: 53548 + management: 46112 + maps: 183610 + mapsLegacy: 116817 + mapsLegacyLicensing: 20214 + ml: 82187 + monitoring: 268612 + navigation: 37269 + newsfeed: 42228 + observability: 89709 + painlessLab: 179748 + regionMap: 66098 + remoteClusters: 51327 + reporting: 183418 + rollup: 97204 + savedObjects: 108518 + savedObjectsManagement: 100503 + searchprofiler: 67080 + security: 189428 + securityOss: 30806 + securitySolution: 622387 + share: 99061 + snapshotRestore: 79032 + spaces: 387915 + telemetry: 91832 + telemetryManagementSection: 52443 + tileMap: 65337 + timelion: 29920 + transform: 41007 + triggersActionsUi: 170001 + uiActions: 97717 + uiActionsEnhanced: 349511 + upgradeAssistant: 81241 + uptime: 40825 + urlDrilldown: 34174 + urlForwarding: 32579 + usageCollection: 39762 + visDefaultEditor: 50178 + visTypeMarkdown: 30896 + visTypeMetric: 42790 + visTypeTable: 94934 + visTypeTagcloud: 37575 + visTypeTimelion: 51933 + visTypeTimeseries: 155203 + visTypeVega: 153573 + visTypeVislib: 242838 + visTypeXy: 20255 + visualizations: 295025 + visualize: 57431 + watcher: 43598 diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 52f9349aec696..c9e414dbc5177 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.11.6", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "@kbn/std": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", @@ -22,6 +23,7 @@ "cpy": "^8.0.0", "core-js": "^3.6.5", "css-loader": "^3.4.2", + "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", "file-loader": "^4.2.0", @@ -38,6 +40,7 @@ "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "rxjs": "^6.5.5", + "js-yaml": "^3.14.0", "sass-loader": "^8.0.2", "source-map-support": "^0.5.19", "style-loader": "^1.1.3", diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index dcfb56be66efd..28b3e37380b4e 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -22,12 +22,14 @@ import 'source-map-support/register'; import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; +import { lastValueFrom } from '@kbn/std'; import { run, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; +import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; run( async ({ log, flags }) => { @@ -93,21 +95,42 @@ run( throw createFlagError('expected --filter to be one or more strings'); } + const focus = typeof flags.focus === 'string' ? [flags.focus] : flags.focus; + if (!Array.isArray(focus) || !focus.every((f) => typeof f === 'string')) { + throw createFlagError('expected --focus to be one or more strings'); + } + + const validateLimits = flags['validate-limits'] ?? false; + if (typeof validateLimits !== 'boolean') { + throw createFlagError('expected --validate-limits to have no value'); + } + + const updateLimits = flags['update-limits'] ?? false; + if (typeof updateLimits !== 'boolean') { + throw createFlagError('expected --update-limits to have no value'); + } + const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, watch, maxWorkerCount, - oss, - dist, + oss: oss && !(validateLimits || updateLimits), + dist: dist || updateLimits, cache, - examples, + examples: examples && !(validateLimits || updateLimits), profileWebpack, extraPluginScanDirs, inspectWorkers, includeCoreBundle, filter, + focus, }); + if (validateLimits) { + validateLimitsForAllBundles(log, config); + return; + } + let update$ = runOptimizer(config); if (reportStats) { @@ -120,7 +143,11 @@ run( update$ = update$.pipe(reportOptimizerStats(reporter, config, log)); } - await update$.pipe(logOptimizerState(log, config)).toPromise(); + await lastValueFrom(update$.pipe(logOptimizerState(log, config))); + + if (updateLimits) { + updateBundleLimits(log, config); + } }, { flags: { @@ -134,6 +161,8 @@ run( 'profile', 'inspect-workers', 'report-stats', + 'validate-limits', + 'update-limits', ], string: ['workers', 'scan-dir', 'filter'], default: { @@ -142,6 +171,7 @@ run( cache: true, 'inspect-workers': true, filter: [], + focus: [], }, help: ` --watch run the optimizer in watch mode @@ -150,12 +180,15 @@ run( --profile profile the webpack builds and write stats.json files to build outputs --no-core disable generating the core bundle --no-cache disable the cache + --focus just like --filter, except dependencies are automatically included, --filter applies to result --filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported --no-examples don't build the example plugins - --dist create bundles that are suitable for inclusion in the Kibana distributable + --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name + --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle + --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb `, }, } diff --git a/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts index 7458fa13eccb3..8f5a01a4e4a5d 100644 --- a/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts +++ b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts @@ -18,20 +18,21 @@ */ import * as Rx from 'rxjs'; -import { toArray, take } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; +import { allValuesFrom } from './rxjs_helpers'; import { summarizeEventStream } from './event_stream_helpers'; it('emits each state with each event, ignoring events when summarizer returns undefined', async () => { const event$ = Rx.of(1, 2, 3, 4, 5); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event) => { - if (event % 2) { - return state + event; - } - }) - .pipe(toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event) => { + if (event % 2) { + return state + event; + } + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -57,15 +58,15 @@ it('emits each state with each event, ignoring events when summarizer returns un it('interleaves injected events when source is synchronous', async () => { const event$ = Rx.of(1, 7); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 5) { - injectEvent(event + 2); - } + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 5) { + injectEvent(event + 2); + } - return state + event; - }) - .pipe(toArray()) - .toPromise(); + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -95,15 +96,15 @@ it('interleaves injected events when source is synchronous', async () => { it('interleaves injected events when source is asynchronous', async () => { const event$ = Rx.of(1, 7, Rx.asyncScheduler); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 5) { - injectEvent(event + 2); - } + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 5) { + injectEvent(event + 2); + } - return state + event; - }) - .pipe(toArray()) - .toPromise(); + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -133,17 +134,17 @@ it('interleaves injected events when source is asynchronous', async () => { it('interleaves mulitple injected events in order', async () => { const event$ = Rx.of(1); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 10) { - injectEvent(10); - injectEvent(20); - injectEvent(30); - } - - return state + event; - }) - .pipe(toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 10) { + injectEvent(10); + injectEvent(20); + injectEvent(30); + } + + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -179,9 +180,9 @@ it('stops an infinite stream when unsubscribed', async () => { return prev + event; }); - const values = await summarizeEventStream(event$, initial, summarize) - .pipe(take(11), toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, summarize).pipe(take(11)) + ); expect(values).toMatchInlineSnapshot(` Array [ diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts index dda66c999b8f1..457da9290bbd0 100644 --- a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts @@ -19,6 +19,7 @@ import * as Rx from 'rxjs'; import { toArray, map } from 'rxjs/operators'; +import { lastValueFrom } from '@kbn/std'; import { pipeClosure, debounceTimeBuffer, maybeMap, maybe } from './rxjs_helpers'; @@ -36,21 +37,21 @@ describe('pipeClosure()', () => { toArray() ); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 1, 2, 3, ] `); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 2, 4, 6, ] `); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 3, 6, @@ -64,7 +65,7 @@ describe('maybe()', () => { it('filters out undefined values from the stream', async () => { const foo$ = Rx.of(1, undefined, 2, undefined, 3).pipe(maybe(), toArray()); - await expect(foo$.toPromise()).resolves.toEqual([1, 2, 3]); + await expect(lastValueFrom(foo$)).resolves.toEqual([1, 2, 3]); }); }); @@ -75,7 +76,7 @@ describe('maybeMap()', () => { toArray() ); - await expect(foo$.toPromise()).resolves.toEqual([1, 3, 5]); + await expect(lastValueFrom(foo$)).resolves.toEqual([1, 3, 5]); }); }); diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.ts index c6385c22518aa..49bf2d8f145dd 100644 --- a/packages/kbn-optimizer/src/common/rxjs_helpers.ts +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.ts @@ -18,7 +18,8 @@ */ import * as Rx from 'rxjs'; -import { mergeMap, tap, debounceTime, map } from 'rxjs/operators'; +import { mergeMap, tap, debounceTime, map, toArray } from 'rxjs/operators'; +import { firstValueFrom } from '@kbn/std'; type Operator = (source: Rx.Observable) => Rx.Observable; type MapFn = (item: T1, index: number) => T2; @@ -73,3 +74,6 @@ export const debounceTimeBuffer = (ms: number) => }) ); }); + +export const allValuesFrom = (observable: Rx.Observable) => + firstValueFrom(observable.pipe(toArray())); diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 549e4b13a4ac0..c522ff770d369 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -22,3 +22,4 @@ export * from './run_optimizer'; export * from './log_optimizer_state'; export * from './report_optimizer_stats'; export * from './node'; +export * from './limits'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 038beca703720..cb5bb1e8fc529 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,7 @@ OptimizerConfig { "cache": true, "dist": false, "inspectWorkers": false, + "limits": "", "maxWorkerCount": 1, "plugins": Array [ Object { diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index de3838eb92975..a89f84e5c543d 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -24,10 +24,18 @@ import { inspect } from 'util'; import cpy from 'cpy'; import del from 'del'; -import { toArray, tap, filter } from 'rxjs/operators'; +import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog } from '@kbn/dev-utils'; -import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '@kbn/optimizer'; +import { + runOptimizer, + OptimizerConfig, + OptimizerUpdate, + logOptimizerState, + readLimits, +} from '@kbn/optimizer'; + +import { allValuesFrom } from '../common'; const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); @@ -72,15 +80,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { dist: false, }); + expect(config.limits).toEqual(readLimits()); + (config as any).limits = ''; + expect(config).toMatchSnapshot('OptimizerConfig'); - const msgs = await runOptimizer(config) - .pipe( + const msgs = await allValuesFrom( + runOptimizer(config).pipe( logOptimizerState(log, config), - filter((x) => x.event?.type !== 'worker stdio'), - toArray() + filter((x) => x.event?.type !== 'worker stdio') ) - .toPromise(); + ); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { if (!truth) { @@ -199,17 +209,16 @@ it('uses cache on second run and exist cleanly', async () => { dist: false, }); - const msgs = await runOptimizer(config) - .pipe( + const msgs = await allValuesFrom( + runOptimizer(config).pipe( tap((state) => { if (state.event?.type === 'worker stdio') { // eslint-disable-next-line no-console console.log('worker', state.event.stream, state.event.line); } - }), - toArray() + }) ) - .toPromise(); + ); expect(msgs.map((m) => m.state.phase)).toMatchInlineSnapshot(` Array [ @@ -231,7 +240,7 @@ it('prepares assets for distribution', async () => { dist: true, }); - await runOptimizer(config).pipe(logOptimizerState(log, config), toArray()).toPromise(); + await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( diff --git a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts index 48cab508954a0..00e6782128dd9 100644 --- a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts @@ -21,12 +21,11 @@ import Path from 'path'; import cpy from 'cpy'; import del from 'del'; -import { toArray } from 'rxjs/operators'; import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getMtimes } from '../optimizer/get_mtimes'; import { OptimizerConfig } from '../optimizer/optimizer_config'; -import { Bundle } from '../common/bundle'; +import { allValuesFrom, Bundle } from '../common'; import { getBundleCacheEvent$ } from '../optimizer/bundle_cache'; const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); @@ -78,9 +77,7 @@ it('emits "bundle cached" event when everything is updated', async () => { bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -119,9 +116,7 @@ it('emits "bundle not cached" event when cacheKey is up to date but caching is d bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -160,9 +155,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is missing', async () bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -201,9 +194,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -247,9 +238,7 @@ it('emits "bundle not cached" event when bundleRefExportIds is outdated, include bundleRefExportIds: ['plugin/bar/public'], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -292,9 +281,7 @@ it('emits "bundle not cached" event when cacheKey is missing', async () => { bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -333,9 +320,7 @@ it('emits "bundle not cached" event when cacheKey is outdated', async () => { jest.spyOn(bundle, 'createCacheKey').mockImplementation(() => 'new'); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ diff --git a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts index 176b17c979da9..00f3c780adc0a 100644 --- a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts @@ -20,6 +20,7 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import ActualWatchpack from 'watchpack'; +import { lastValueFrom } from '@kbn/std'; import { Bundle, ascending } from '../common'; import { watchBundlesForChanges$ } from '../optimizer/watch_bundles_for_changes'; @@ -78,8 +79,8 @@ afterEach(async () => { it('notifies of changes and completes once all bundles have changed', async () => { expect.assertions(18); - const promise = watchBundlesForChanges$(bundleCacheEvent$, Date.now()) - .pipe( + const promise = lastValueFrom( + watchBundlesForChanges$(bundleCacheEvent$, Date.now()).pipe( map((event, i) => { // each time we trigger a change event we get a 'changed detected' event if (i === 0 || i === 2 || i === 4 || i === 6) { @@ -116,7 +117,7 @@ it('notifies of changes and completes once all bundles have changed', async () = } }) ) - .toPromise(); + ); expect(MockWatchPack.mock.instances).toHaveLength(1); const [watcher] = (MockWatchPack.mock.instances as any) as Array>; diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts new file mode 100644 index 0000000000000..b0fae0901251d --- /dev/null +++ b/packages/kbn-optimizer/src/limits.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 Fs from 'fs'; + +import dedent from 'dedent'; +import Yaml from 'js-yaml'; +import { createFailError, ToolingLog } from '@kbn/dev-utils'; + +import { OptimizerConfig, getMetrics, Limits } from './optimizer'; + +const LIMITS_PATH = require.resolve('../limits.yml'); +const DEFAULT_BUDGET = 15000; + +const diff = (a: T[], b: T[]): T[] => a.filter((item) => !b.includes(item)); + +export function readLimits(): Limits { + let yaml; + try { + yaml = Fs.readFileSync(LIMITS_PATH, 'utf8'); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + return yaml ? Yaml.safeLoad(yaml) : {}; +} + +export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { + const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); + const configBundleIds = config.bundles.map((b) => b.id); + + const missingBundleIds = diff(configBundleIds, limitBundleIds); + const extraBundleIds = diff(limitBundleIds, configBundleIds); + + const issues = []; + if (missingBundleIds.length) { + issues.push(`missing: ${missingBundleIds.join(', ')}`); + } + if (extraBundleIds.length) { + issues.push(`extra: ${extraBundleIds.join(', ')}`); + } + if (issues.length) { + throw createFailError( + dedent` + The limits defined in packages/kbn-optimizer/limits.yml are outdated. Please update + this file with a limit (in bytes) for every production bundle. + + ${issues.join('\n ')} + + To automatically update the limits file locally run: + + node scripts/build_kibana_platform_plugins.js --update-limits + + To validate your changes locally run: + + node scripts/build_kibana_platform_plugins.js --validate-limits + ` + '\n' + ); + } + + log.success('limits.yml file valid'); +} + +export function updateBundleLimits(log: ToolingLog, config: OptimizerConfig) { + const metrics = getMetrics(log, config); + + const pageLoadAssetSize: NonNullable = {}; + + for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { + if (metric.group === 'page load bundle size') { + const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; + pageLoadAssetSize[metric.id] = + existingLimit != null && existingLimit >= metric.value + ? existingLimit + : metric.value + DEFAULT_BUDGET; + } + } + + const newLimits: Limits = { + pageLoadAssetSize, + }; + + Fs.writeFileSync(LIMITS_PATH, Yaml.safeDump(newLimits)); + log.success(`wrote updated limits to ${LIMITS_PATH}`); +} diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index 7fbf009e38a7d..1ce3b9eeeafd0 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -64,6 +64,7 @@ export class Cache { this.codes = LmdbStore.open({ name: 'codes', path: CACHE_DIR, + maxReaders: 500, }); this.atimes = this.codes.openDB({ diff --git a/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts new file mode 100644 index 0000000000000..0e31899e6e425 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import { focusBundles } from './focus_bundles'; +import { Bundle } from '../common'; + +function createBundle(id: string, deps: ReturnType) { + const bundle = new Bundle({ + type: id === 'core' ? 'entry' : 'plugin', + id, + contextDir: Path.resolve('/kibana/plugins', id), + outputDir: Path.resolve('/kibana/plugins', id, 'target/public'), + publicDirNames: ['public'], + sourceRoot: Path.resolve('/kibana'), + }); + + jest.spyOn(bundle, 'readBundleDeps').mockReturnValue(deps); + + return bundle; +} + +const BUNDLES = [ + createBundle('core', { + implicit: [], + explicit: [], + }), + createBundle('foo', { + implicit: ['core'], + explicit: [], + }), + createBundle('bar', { + implicit: ['core'], + explicit: ['foo'], + }), + createBundle('baz', { + implicit: ['core'], + explicit: ['bar'], + }), + createBundle('box', { + implicit: ['core'], + explicit: ['foo'], + }), +]; + +function test(filters: string[]) { + return focusBundles(filters, BUNDLES) + .map((b) => b.id) + .sort((a, b) => a.localeCompare(b)) + .join(', '); +} + +it('returns all bundles when no focus filters are defined', () => { + expect(test([])).toMatchInlineSnapshot(`"bar, baz, box, core, foo"`); +}); + +it('includes a single instance of all implicit and explicit dependencies', () => { + expect(test(['core'])).toMatchInlineSnapshot(`"core"`); + expect(test(['foo'])).toMatchInlineSnapshot(`"core, foo"`); + expect(test(['bar'])).toMatchInlineSnapshot(`"bar, core, foo"`); + expect(test(['baz'])).toMatchInlineSnapshot(`"bar, baz, core, foo"`); + expect(test(['box'])).toMatchInlineSnapshot(`"box, core, foo"`); +}); diff --git a/packages/kbn-optimizer/src/optimizer/focus_bundles.ts b/packages/kbn-optimizer/src/optimizer/focus_bundles.ts new file mode 100644 index 0000000000000..67c6d02364668 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/focus_bundles.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 { Bundle } from '../common'; +import { filterById } from './filter_by_id'; + +export function focusBundles(filters: string[], bundles: Bundle[]) { + if (!filters.length) { + return [...bundles]; + } + + const queue = new Set(filterById(filters, bundles)); + const focused: Bundle[] = []; + + for (const bundle of queue) { + focused.push(bundle); + + const { explicit, implicit } = bundle.readBundleDeps(); + const depIds = [...explicit, ...implicit]; + if (depIds.length) { + for (const dep of filterById(depIds, bundles)) { + queue.add(dep); + } + } + } + + return focused; +} diff --git a/packages/kbn-optimizer/src/optimizer/get_mtimes.ts b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts index 07777c323637a..b6c3678709880 100644 --- a/packages/kbn-optimizer/src/optimizer/get_mtimes.ts +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts @@ -20,7 +20,8 @@ import Fs from 'fs'; import * as Rx from 'rxjs'; -import { mergeMap, toArray, map, catchError } from 'rxjs/operators'; +import { mergeMap, map, catchError } from 'rxjs/operators'; +import { allValuesFrom } from '../common'; const stat$ = Rx.bindNodeCallback(Fs.stat); @@ -28,20 +29,22 @@ const stat$ = Rx.bindNodeCallback(Fs.stat); * get mtimes of referenced paths concurrently, limit concurrency to 100 */ export async function getMtimes(paths: Iterable) { - return await Rx.from(paths) - .pipe( - // map paths to [path, mtimeMs] entries with concurrency of - // 100 at a time, ignoring missing paths - mergeMap( - (path) => - stat$(path).pipe( - map((stat) => [path, stat.mtimeMs] as const), - catchError((error: any) => (error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error))) - ), - 100 - ), - toArray(), - map((entries) => new Map(entries)) + return new Map( + await allValuesFrom( + Rx.from(paths).pipe( + // map paths to [path, mtimeMs] entries with concurrency of + // 100 at a time, ignoring missing paths + mergeMap( + (path) => + stat$(path).pipe( + map((stat) => [path, stat.mtimeMs] as const), + catchError((error: any) => + error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error) + ) + ), + 100 + ) + ) ) - .toPromise(); + ); } diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts new file mode 100644 index 0000000000000..cc4cd05f42c3f --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 Fs from 'fs'; +import Path from 'path'; + +import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; +import { OptimizerConfig } from './optimizer_config'; + +const flatten = (arr: Array): T[] => + arr.reduce((acc: T[], item) => acc.concat(item), []); + +interface Entry { + relPath: string; + stats: Fs.Stats; +} + +const IGNORED_EXTNAME = ['.map', '.br', '.gz']; + +const getFiles = (dir: string, parent?: string) => + flatten( + Fs.readdirSync(dir).map((name): Entry | Entry[] => { + const absPath = Path.join(dir, name); + const relPath = parent ? Path.join(parent, name) : name; + const stats = Fs.statSync(absPath); + + if (stats.isDirectory()) { + return getFiles(absPath, relPath); + } + + return { + relPath, + stats, + }; + }) + ).filter((file) => { + const filename = Path.basename(file.relPath); + if (filename.startsWith('.')) { + return false; + } + + const ext = Path.extname(filename); + if (IGNORED_EXTNAME.includes(ext)) { + return false; + } + + return true; + }); + +export function getMetrics(log: ToolingLog, config: OptimizerConfig) { + return flatten( + config.bundles.map((bundle) => { + // make the cache read from the cache file since it was likely updated by the worker + bundle.cache.refresh(); + + const outputFiles = getFiles(bundle.outputDir); + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = outputFiles.find((f) => f.relPath === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); + const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f)); + + if (asyncChunks.length) { + log.verbose(bundle.id, 'async chunks', asyncChunks); + } + if (miscFiles.length) { + log.verbose(bundle.id, 'misc files', asyncChunks); + } + + const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); + + const bundleMetrics: CiStatsMetrics = [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: bundle.cache.getModuleCount() || 0, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.stats!.size, + limit: config.limits.pageLoadAssetSize?.[bundle.id], + limitConfigPath: `packages/kbn-optimizer/limits.yml`, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + + log.debug(bundle.id, 'metrics', bundleMetrics); + + return bundleMetrics; + }) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts index 6edcde56e26de..b92eee0a51fd5 100644 --- a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts +++ b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts @@ -20,12 +20,11 @@ import * as Rx from 'rxjs'; import { REPO_ROOT } from '@kbn/utils'; -import { Update } from '../common'; +import { Update, allValuesFrom } from '../common'; import { OptimizerState } from './optimizer_state'; import { OptimizerConfig } from './optimizer_config'; import { handleOptimizerCompletion } from './handle_optimizer_completion'; -import { toArray } from 'rxjs/operators'; const createUpdate$ = (phase: OptimizerState['phase']) => Rx.of>({ @@ -44,13 +43,12 @@ const config = (watch?: boolean) => repoRoot: REPO_ROOT, watch, }); -const collect = (stream: Rx.Observable): Promise => stream.pipe(toArray()).toPromise(); it('errors if the optimizer completes when in watch mode', async () => { const update$ = createUpdate$('success'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config(true)))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config(true)))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly completed when in watch mode"` ); @@ -60,7 +58,7 @@ it('errors if the optimizer completes in phase "issue"', async () => { const update$ = createUpdate$('issue'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot(`"webpack issue"`); }); @@ -68,7 +66,7 @@ it('errors if the optimizer completes in phase "initializing"', async () => { const update$ = createUpdate$('initializing'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"initializing\\""` ); @@ -78,7 +76,7 @@ it('errors if the optimizer completes in phase "reallocating"', async () => { const update$ = createUpdate$('reallocating'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"reallocating\\""` ); @@ -88,7 +86,7 @@ it('errors if the optimizer completes in phase "running"', async () => { const update$ = createUpdate$('running'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"running\\""` ); @@ -98,7 +96,7 @@ it('passes through errors on the source stream', async () => { const error = new Error('foo'); const update$ = Rx.throwError(error); - await expect(collect(update$.pipe(handleOptimizerCompletion(config())))).rejects.toThrowError( - error - ); + await expect( + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) + ).rejects.toThrowError(error); }); diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts index 84fd395e98976..77df112b44351 100644 --- a/packages/kbn-optimizer/src/optimizer/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -25,3 +25,4 @@ export * from './watch_bundles_for_changes'; export * from './run_workers'; export * from './bundle_cache'; export * from './handle_optimizer_completion'; +export * from './get_output_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts index 9bf8f9db1fe45..a7c07358fa6d6 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; -import { toArray } from 'rxjs/operators'; +import { allValuesFrom } from '../common'; import { observeStdio$ } from './observe_stdio'; @@ -27,18 +27,18 @@ it('notifies on every line, uncluding partial content at the end without a newli const chunks = [`foo\nba`, `r\nb`, `az`]; await expect( - observeStdio$( - new Readable({ - read() { - this.push(chunks.shift()!); - if (!chunks.length) { - this.push(null); - } - }, - }) + allValuesFrom( + observeStdio$( + new Readable({ + read() { + this.push(chunks.shift()!); + if (!chunks.length) { + this.push(null); + } + }, + }) + ) ) - .pipe(toArray()) - .toPromise() ).resolves.toMatchInlineSnapshot(` Array [ "foo", diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index fd887e8c2c012..c3f3505197038 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -22,6 +22,8 @@ jest.mock('./kibana_platform_plugins.ts'); jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); +jest.mock('./focus_bundles'); +jest.mock('../limits.ts'); jest.mock('os', () => { const realOs = jest.requireActual('os'); @@ -120,6 +122,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -148,6 +151,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -176,6 +180,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -206,6 +211,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -233,6 +239,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -260,6 +267,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -284,6 +292,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -308,6 +317,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -333,6 +343,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -358,6 +369,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -385,6 +397,8 @@ describe('OptimizerConfig::create()', () => { .findKibanaPlatformPlugins; const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles; const filterById: jest.Mock = jest.requireMock('./filter_by_id.ts').filterById; + const focusBundles: jest.Mock = jest.requireMock('./focus_bundles').focusBundles; + const readLimits: jest.Mock = jest.requireMock('../limits.ts').readLimits; beforeEach(() => { if ('mock' in OptimizerConfig.parseOptions) { @@ -398,6 +412,8 @@ describe('OptimizerConfig::create()', () => { findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); + focusBundles.mockReturnValue(Symbol('focused bundles')); + readLimits.mockReturnValue(Symbol('limits')); jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { [key in keyof ParsedOptions]: any; @@ -414,6 +430,7 @@ describe('OptimizerConfig::create()', () => { inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), filters: [], + focus: [], includeCoreBundle: false, })); }); @@ -429,6 +446,7 @@ describe('OptimizerConfig::create()', () => { "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), + "limits": Symbol(limits), "maxWorkerCount": Symbol(parsed max worker count), "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), @@ -466,17 +484,14 @@ describe('OptimizerConfig::create()', () => { "calls": Array [ Array [ Array [], - Array [ - Symbol(bundle1), - Symbol(bundle2), - ], + Symbol(focused bundles), ], ], "instances": Array [ [Window], ], "invocationCallOrder": Array [ - 23, + 24, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b1ab1ebfe49f2..8091f6aa90508 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -32,6 +32,14 @@ import { import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; import { filterById } from './filter_by_id'; +import { focusBundles } from './focus_bundles'; +import { readLimits } from '../limits'; + +export interface Limits { + pageLoadAssetSize?: { + [id: string]: number | undefined; + }; +} function pickMaxWorkerCount(dist: boolean) { // don't break if cpus() returns nothing, or an empty array @@ -97,6 +105,11 @@ interface Options { * --filter f*r # [foobar], excludes [foo, bar] */ filter?: string[]; + /** + * behaves just like filter, but includes required bundles and plugins of the + * listed bundle ids. Filters only apply to bundles selected by focus + */ + focus?: string[]; /** flag that causes the core bundle to be built along with plugins */ includeCoreBundle?: boolean; @@ -125,6 +138,7 @@ export interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; filters: string[]; + focus: string[]; inspectWorkers: boolean; includeCoreBundle: boolean; themeTags: ThemeTags; @@ -141,6 +155,7 @@ export class OptimizerConfig { const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; const includeCoreBundle = !!options.includeCoreBundle; const filters = options.filter || []; + const focus = options.focus || []; const repoRoot = options.repoRoot; if (!Path.isAbsolute(repoRoot)) { @@ -203,6 +218,7 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, filters, + focus, inspectWorkers, includeCoreBundle, themeTags, @@ -229,7 +245,7 @@ export class OptimizerConfig { ]; return new OptimizerConfig( - filterById(options.filters, bundles), + filterById(options.filters, focusBundles(options.focus, bundles)), options.cache, options.watch, options.inspectWorkers, @@ -238,7 +254,8 @@ export class OptimizerConfig { options.maxWorkerCount, options.dist, options.profileWebpack, - options.themeTags + options.themeTags, + readLimits() ); } @@ -252,7 +269,8 @@ export class OptimizerConfig { public readonly maxWorkerCount: number, public readonly dist: boolean, public readonly profileWebpack: boolean, - public readonly themeTags: ThemeTags + public readonly themeTags: ThemeTags, + public readonly limits: Limits ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts index eff2bce0b827e..a0f59a3505e30 100644 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -17,136 +17,41 @@ * under the License. */ -import Fs from 'fs'; -import Path from 'path'; - import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter, CiStatsMetrics, ToolingLog } from '@kbn/dev-utils'; +import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerState, OptimizerConfig } from './optimizer'; +import { OptimizerConfig, getMetrics } from './optimizer'; import { pipeClosure } from './common'; -const flatten = (arr: Array): T[] => - arr.reduce((acc: T[], item) => acc.concat(item), []); - -interface Entry { - relPath: string; - stats: Fs.Stats; -} - -const IGNORED_EXTNAME = ['.map', '.br', '.gz']; - -const getFiles = (dir: string, parent?: string) => - flatten( - Fs.readdirSync(dir).map((name): Entry | Entry[] => { - const absPath = Path.join(dir, name); - const relPath = parent ? Path.join(parent, name) : name; - const stats = Fs.statSync(absPath); - - if (stats.isDirectory()) { - return getFiles(absPath, relPath); - } - - return { - relPath, - stats, - }; - }) - ).filter((file) => { - const filename = Path.basename(file.relPath); - if (filename.startsWith('.')) { - return false; - } - - const ext = Path.extname(filename); - if (IGNORED_EXTNAME.includes(ext)) { - return false; - } - - return true; - }); - export function reportOptimizerStats( reporter: CiStatsReporter, config: OptimizerConfig, log: ToolingLog ) { - return pipeClosure((update$: OptimizerUpdate$) => { - let lastState: OptimizerState | undefined; - return update$.pipe( + return pipeClosure((update$: OptimizerUpdate$) => + update$.pipe( materialize(), mergeMap(async (n) => { - if (n.kind === 'N' && n.value?.state) { - lastState = n.value?.state; - } - - if (n.kind === 'C' && lastState) { - await reporter.metrics( - flatten( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - const outputFiles = getFiles(bundle.outputDir); - const entryName = `${bundle.id}.${bundle.type}.js`; - const entry = outputFiles.find((f) => f.relPath === entryName); - if (!entry) { - throw new Error( - `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` - ); - } - - const chunkPrefix = `${bundle.id}.chunk.`; - const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); - const miscFiles = outputFiles.filter( - (f) => f !== entry && !asyncChunks.includes(f) - ); - - if (asyncChunks.length) { - log.verbose(bundle.id, 'async chunks', asyncChunks); - } - if (miscFiles.length) { - log.verbose(bundle.id, 'misc files', asyncChunks); - } - - const sumSize = (files: Entry[]) => - files.reduce((acc: number, f) => acc + f.stats!.size, 0); - - const metrics: CiStatsMetrics = [ - { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }, - { - group: `page load bundle size`, - id: bundle.id, - value: entry.stats!.size, - }, - { - group: `async chunks size`, - id: bundle.id, - value: sumSize(asyncChunks), - }, - { - group: `miscellaneous assets size`, - id: bundle.id, - value: sumSize(miscFiles), - }, - ]; - - log.info(bundle.id, 'metrics', metrics); - - return metrics; - }) - ) - ); + if (n.kind === 'C') { + const metrics = getMetrics(log, config); + + await reporter.metrics(metrics); + + for (const metric of metrics) { + if (metric.limit != null && metric.value > metric.limit) { + const value = metric.value.toLocaleString(); + const limit = metric.limit.toLocaleString(); + log.warning( + `Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]` + ); + } + } } return n; }), dematerialize() - ); - }); + ) + ); } diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index d78eb8214f607..0024d2801d173 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -182,7 +182,7 @@ const observeCompiler = ( ); bundle.cache.set({ - bundleRefExportIds, + bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), moduleCount, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 48dba22505232..2e50f4214beb4 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -196,6 +196,7 @@ function help() { --oss Do not include the x-pack when running command. --skip-kibana-plugins Filter all plugins in ./plugins and ../kibana-extra when running command. --no-cache Disable the bootstrap cache + --no-validate Disable the bootstrap yarn.lock validation --verbose Set log level to verbose --debug Set log level to debug --quiet Set log level to error @@ -222,9 +223,10 @@ async function run(argv) { i: 'include' }, default: { - cache: true + cache: true, + validate: true }, - boolean: ['prefer-offline', 'frozen-lockfile', 'cache'] + boolean: ['prefer-offline', 'frozen-lockfile', 'cache', 'validate'] }); const args = options._; @@ -8998,7 +9000,11 @@ const BootstrapCommand = { } const yarnLock = await Object(_utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__["readYarnLock"])(kbn); - await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + + if (options.validate) { + await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + } + await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_0__["linkProjectExecutables"])(projects, projectGraph); /** * At the end of the bootstrapping process we call all `kbn:bootstrap` scripts diff --git a/packages/kbn-pm/src/cli.ts b/packages/kbn-pm/src/cli.ts index 816e84c13bbe9..92ddf3d957cd5 100644 --- a/packages/kbn-pm/src/cli.ts +++ b/packages/kbn-pm/src/cli.ts @@ -47,6 +47,7 @@ function help() { --oss Do not include the x-pack when running command. --skip-kibana-plugins Filter all plugins in ./plugins and ../kibana-extra when running command. --no-cache Disable the bootstrap cache + --no-validate Disable the bootstrap yarn.lock validation --verbose Set log level to verbose --debug Set log level to debug --quiet Set log level to error @@ -80,8 +81,9 @@ export async function run(argv: string[]) { }, default: { cache: true, + validate: true, }, - boolean: ['prefer-offline', 'frozen-lockfile', 'cache'], + boolean: ['prefer-offline', 'frozen-lockfile', 'cache', 'validate'], }); const args = options._; diff --git a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap b/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap deleted file mode 100644 index be146d710c87a..0000000000000 --- a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: link bins 1`] = ` -Array [ - Array [ - Map { - "kibana" => Project { - "allDependencies": Object { - "bar": "1.0.0", - }, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": true, - "json": Object { - "dependencies": Object { - "bar": "1.0.0", - }, - "name": "kibana", - "version": "1.0.0", - "workspaces": Object { - "packages": Array [ - "packages/*", - ], - }, - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/package.json", - "path": "/packages/kbn-pm/src/commands", - "productionDependencies": Object { - "bar": "1.0.0", - }, - "scripts": Object {}, - "targetLocation": "/packages/kbn-pm/src/commands/target", - "version": "1.0.0", - }, - "bar" => Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - }, - Map { - "kibana" => Array [ - Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - ], - "bar" => Array [], - }, - ], -] -`; - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: script 1`] = ` -Array [ - Array [ - Object { - "args": Array [], - "debug": undefined, - "pkg": Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - "script": "kbn:bootstrap", - }, - ], -] -`; - -exports[`does not run installer if no deps in package: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; - -exports[`handles "frozen-lockfile": install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [ - "--frozen-lockfile", - ], - ], -] -`; - -exports[`handles dependencies of dependencies: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], - Array [ - "/packages/kbn-pm/src/commands/packages/bar", - Array [], - ], - Array [ - "/packages/kbn-pm/src/commands/packages/foo", - Array [], - ], -] -`; diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts deleted file mode 100644 index 956c4e72933ba..0000000000000 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ /dev/null @@ -1,246 +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. - */ - -jest.mock('../utils/scripts'); -jest.mock('../utils/link_project_executables'); -jest.mock('../utils/validate_yarn_lock'); - -import { resolve } from 'path'; - -import { ToolingLogCollectingWriter } from '@kbn/dev-utils/tooling_log'; - -import { absolutePathSnapshotSerializer, stripAnsiSnapshotSerializer } from '../test_helpers'; -import { linkProjectExecutables } from '../utils/link_project_executables'; -import { IPackageJson } from '../utils/package_json'; -import { Project } from '../utils/project'; -import { buildProjectGraph } from '../utils/projects'; -import { installInDir, runScriptInPackageStreaming, yarnWorkspacesInfo } from '../utils/scripts'; -import { BootstrapCommand } from './bootstrap'; -import { Kibana } from '../utils/kibana'; -import { log } from '../utils/log'; - -const mockInstallInDir = installInDir as jest.Mock; -const mockRunScriptInPackageStreaming = runScriptInPackageStreaming as jest.Mock; -const mockLinkProjectExecutables = linkProjectExecutables as jest.Mock; -const mockYarnWorkspacesInfo = yarnWorkspacesInfo as jest.Mock; - -const logWriter = new ToolingLogCollectingWriter('debug'); -log.setLogLevel('silent'); -log.setWriters([logWriter]); -beforeEach(() => { - logWriter.messages.length = 0; -}); - -const createProject = (packageJson: IPackageJson, path = '.') => { - const project = new Project( - { - name: 'kibana', - version: '1.0.0', - ...packageJson, - }, - resolve(__dirname, path) - ); - - if (packageJson.workspaces) { - project.isWorkspaceRoot = true; - } - - return project; -}; -expect.addSnapshotSerializer(absolutePathSnapshotSerializer); -expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); - -beforeEach(() => { - mockYarnWorkspacesInfo.mockResolvedValue({}); -}); - -afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); -}); - -test('handles dependencies of dependencies', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - const foo = createProject( - { - dependencies: { - bar: 'link:../bar', - }, - name: 'foo', - }, - 'packages/foo' - ); - const bar = createProject( - { - dependencies: { - baz: 'link:../baz', - }, - name: 'bar', - }, - 'packages/bar' - ); - const baz = createProject( - { - name: 'baz', - }, - 'packages/baz' - ); - - const projects = new Map([ - ['kibana', kibana], - ['foo', foo], - ['bar', bar], - ['baz', baz], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - info [bar] running yarn, - "", - "", - info [foo] running yarn, - "", - "", - ] - `); -}); - -test('does not run installer if no deps in package', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - // bar has no dependencies - const bar = createProject( - { - name: 'bar', - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('handles "frozen-lockfile"', async () => { - const kibana = createProject({ - dependencies: { - foo: '2.2.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - - const projects = new Map([['kibana', kibana]]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: { - 'frozen-lockfile': true, - }, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); -}); - -test('calls "kbn:bootstrap" scripts and links executables after installing deps', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - const bar = createProject( - { - name: 'bar', - scripts: { - 'kbn:bootstrap': 'node ./bar.js', - }, - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockLinkProjectExecutables.mock.calls).toMatchSnapshot('link bins'); - expect(mockRunScriptInPackageStreaming.mock.calls).toMatchSnapshot('script'); -}); diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 7cf89c5f08f96..0fa3f355ae9d6 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -58,7 +58,9 @@ export const BootstrapCommand: ICommand = { const yarnLock = await readYarnLock(kbn); - await validateYarnLock(kbn, yarnLock); + if (options.validate) { + await validateYarnLock(kbn, yarnLock); + } await linkProjectExecutables(projects, projectGraph); diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts index 23dcb302f090d..22b9713b78332 100644 --- a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts @@ -35,7 +35,8 @@ it('extracts expected components from html', () => { ## Release Note: Checkout this feature - `) + `), + 'release note' ) ).toMatchInlineSnapshot(`"Checkout this feature"`); @@ -46,10 +47,11 @@ it('extracts expected components from html', () => { Fixes: #1234 - #### Release Note: + #### Dev docs: We fixed an issue - `) + `), + 'dev docs' ) ).toMatchInlineSnapshot(`"We fixed an issue"`); @@ -60,8 +62,9 @@ it('extracts expected components from html', () => { Fixes: #1234 - Release note: Checkout feature foo - `) + OTHER TITLE: Checkout feature foo + `), + 'other title' ) ).toMatchInlineSnapshot(`"Checkout feature foo"`); @@ -73,7 +76,8 @@ it('extracts expected components from html', () => { My PR description release note : bar - `) + `), + 'release note' ) ).toMatchInlineSnapshot(`"bar"`); }); diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.ts index 57df203470a5a..0d9135c431e36 100644 --- a/packages/kbn-release-notes/src/lib/get_note_from_description.ts +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.ts @@ -19,11 +19,12 @@ import cheerio from 'cheerio'; -export function getNoteFromDescription(descriptionHtml: string) { +export function getNoteFromDescription(descriptionHtml: string, header: string) { + const re = new RegExp(`^(\\s*${header.toLowerCase()}(?:s)?\\s*:?\\s*)`, 'i'); const $ = cheerio.load(descriptionHtml); for (const el of $('p,h1,h2,h3,h4,h5').toArray()) { const text = $(el).text(); - const match = text.match(/^(\s*release note(?:s)?\s*:?\s*)/i); + const match = text.match(re); if (!match) { continue; diff --git a/packages/kbn-release-notes/src/lib/pr_api.ts b/packages/kbn-release-notes/src/lib/pr_api.ts index 1f26aa7ad86c3..5fa3dfdba10ef 100644 --- a/packages/kbn-release-notes/src/lib/pr_api.ts +++ b/packages/kbn-release-notes/src/lib/pr_api.ts @@ -178,7 +178,9 @@ export class PrApi { versions: labels .map((l) => Version.fromLabel(l)) .filter((v): v is Version => v instanceof Version), - note: getNoteFromDescription(node.bodyHTML), + note: + getNoteFromDescription(node.bodyHTML, 'release note') || + getNoteFromDescription(node.bodyHTML, 'dev docs'), }; } diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index d9d3ec4b0d52b..c111428017539 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -27,3 +27,4 @@ export { withTimeout } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; +export * from './rxjs_7'; diff --git a/packages/kbn-std/src/rxjs_7.test.ts b/packages/kbn-std/src/rxjs_7.test.ts new file mode 100644 index 0000000000000..ff1026e23b7ef --- /dev/null +++ b/packages/kbn-std/src/rxjs_7.test.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; + +import { firstValueFrom, lastValueFrom } from './rxjs_7'; + +// create an empty observable that completes with no notifications +// after a delay to ensure helpers aren't checking for the EMPTY constant +function empty() { + return new Rx.Observable((subscriber) => { + setTimeout(() => { + subscriber.complete(); + }, 0); + }); +} + +describe('firstValueFrom()', () => { + it('resolves to the first value from the observable', async () => { + await expect(firstValueFrom(Rx.of(1, 2, 3))).resolves.toBe(1); + }); + + it('rejects if the observable is empty', async () => { + await expect(firstValueFrom(empty())).rejects.toThrowErrorMatchingInlineSnapshot( + `"no elements in sequence"` + ); + }); + + it('unsubscribes from a source observable that emits synchronously', async () => { + const values = [1, 2, 3, 4]; + let unsubscribed = false; + const source = new Rx.Observable((subscriber) => { + while (!subscriber.closed && values.length) { + subscriber.next(values.shift()!); + } + unsubscribed = subscriber.closed; + subscriber.complete(); + }); + + await expect(firstValueFrom(source)).resolves.toMatchInlineSnapshot(`1`); + if (!unsubscribed) { + throw new Error('expected source to be unsubscribed'); + } + expect(values).toEqual([2, 3, 4]); + }); + + it('unsubscribes from the source observable after first async notification', async () => { + const values = [1, 2, 3, 4]; + let unsubscribed = false; + const source = new Rx.Observable((subscriber) => { + setTimeout(() => { + while (!subscriber.closed) { + subscriber.next(values.shift()!); + } + unsubscribed = subscriber.closed; + }); + }); + + await expect(firstValueFrom(source)).resolves.toMatchInlineSnapshot(`1`); + if (!unsubscribed) { + throw new Error('expected source to be unsubscribed'); + } + expect(values).toEqual([2, 3, 4]); + }); +}); + +describe('lastValueFrom()', () => { + it('resolves to the last value from the observable', async () => { + await expect(lastValueFrom(Rx.of(1, 2, 3))).resolves.toBe(3); + }); + + it('rejects if the observable is empty', async () => { + await expect(lastValueFrom(empty())).rejects.toThrowErrorMatchingInlineSnapshot( + `"no elements in sequence"` + ); + }); +}); diff --git a/packages/kbn-std/src/rxjs_7.ts b/packages/kbn-std/src/rxjs_7.ts new file mode 100644 index 0000000000000..cb10f9de880fd --- /dev/null +++ b/packages/kbn-std/src/rxjs_7.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 { Observable } from 'rxjs'; +import { first, last } from 'rxjs/operators'; + +export function firstValueFrom(source: Observable) { + // we can't use SafeSubscriber the same way that RxJS 7 does, so instead we + return source.pipe(first()).toPromise(); +} + +export function lastValueFrom(source: Observable) { + return source.pipe(last()).toPromise(); +} diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts index 87ba68c1bcb27..9f7ff5ac80c9f 100644 --- a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts +++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts @@ -88,7 +88,7 @@ export function runTelemetryCheck() { task: (context) => new Listr(checkCompatibleTypesTask(context), { exitOnError: true }), }, { - enabled: (_) => !!ignoreStoredJson, + enabled: (_) => fix || !ignoreStoredJson, title: 'Checking Matching collector.schema against stored json files', task: (context) => new Listr(checkMatchingSchemasTask(context, !fix), { exitOnError: true }), @@ -96,7 +96,10 @@ export function runTelemetryCheck() { { enabled: (_) => fix, skip: ({ roots }: TaskContext) => { - return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length); + const noDiffs = roots.every( + ({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length + ); + return noDiffs && 'No changes needed.'; }, title: 'Generating new telemetry mappings', task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }), @@ -104,7 +107,10 @@ export function runTelemetryCheck() { { enabled: (_) => fix, skip: ({ roots }: TaskContext) => { - return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length); + const noDiffs = roots.every( + ({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length + ); + return noDiffs && 'No changes needed.'; }, title: 'Updating telemetry mapping files', task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }), diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap index 5b1b3d9d35299..829b45496925c 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap @@ -1,5 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`parseUsageCollection throws when \`makeUsageCollector\` argument is a function call 1`] = ` +"Error extracting collector in src/fixtures/telemetry_collectors/externally_defined_usage_collector/index.ts +Error: makeUsageCollector argument must be an object." +`; + exports[`parseUsageCollection throws when mapping fields is not defined 1`] = ` "Error extracting collector in src/fixtures/telemetry_collectors/unmapped_collector.ts Error: usageCollector.schema must be defined." diff --git a/packages/kbn-telemetry-tools/src/tools/config.test.ts b/packages/kbn-telemetry-tools/src/tools/config.test.ts index 51ca0493cbb5a..ad2d1bce885ea 100644 --- a/packages/kbn-telemetry-tools/src/tools/config.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/config.test.ts @@ -33,7 +33,10 @@ describe('parseTelemetryRC', () => { { root: configRoot, output: configRoot, - exclude: [path.resolve(configRoot, './unmapped_collector.ts')], + exclude: [ + path.resolve(configRoot, './unmapped_collector.ts'), + path.resolve(configRoot, './externally_defined_usage_collector/index.ts'), + ], }, ]); }); diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts index d036b93a7bbf9..64d7f1fc35f47 100644 --- a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts @@ -33,7 +33,7 @@ export function loadFixtureProgram(fixtureName: string) { 'src', 'fixtures', 'telemetry_collectors', - `${fixtureName}.ts` + `${fixtureName}` ); const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json'); if (!tsConfig) { @@ -52,49 +52,56 @@ describe('parseUsageCollection', () => { it.todo('throws when a function is returned from fetch'); it.todo('throws when an object is not returned from fetch'); + it('throws when `makeUsageCollector` argument is a function call', () => { + const { program, sourceFile } = loadFixtureProgram( + 'externally_defined_usage_collector/index.ts' + ); + expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot(); + }); + it('throws when mapping fields is not defined', () => { - const { program, sourceFile } = loadFixtureProgram('unmapped_collector'); + const { program, sourceFile } = loadFixtureProgram('unmapped_collector.ts'); expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot(); }); it('parses root level defined collector', () => { - const { program, sourceFile } = loadFixtureProgram('working_collector'); + const { program, sourceFile } = loadFixtureProgram('working_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual([parsedWorkingCollector]); }); it('parses collector with schema defined as union of spreads', () => { - const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector'); + const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual([parsedSchemaDefinedWithSpreadsCollector]); }); it('parses nested collectors', () => { - const { program, sourceFile } = loadFixtureProgram('nested_collector'); + const { program, sourceFile } = loadFixtureProgram('nested_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual([parsedNestedCollector]); }); it('parses imported schema property', () => { - const { program, sourceFile } = loadFixtureProgram('imported_schema'); + const { program, sourceFile } = loadFixtureProgram('imported_schema.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual(parsedImportedSchemaCollector); }); it('parses externally defined collectors', () => { - const { program, sourceFile } = loadFixtureProgram('externally_defined_collector'); + const { program, sourceFile } = loadFixtureProgram('externally_defined_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual(parsedExternallyDefinedCollector); }); it('parses imported Usage interface', () => { - const { program, sourceFile } = loadFixtureProgram('imported_usage_interface'); + const { program, sourceFile } = loadFixtureProgram('imported_usage_interface.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual(parsedImportedUsageInterface); }); it('skips files that do not define a collector', () => { - const { program, sourceFile } = loadFixtureProgram('file_with_no_collector'); + const { program, sourceFile } = loadFixtureProgram('file_with_no_collector.ts'); const result = [...parseUsageCollection(sourceFile, program)]; expect(result).toEqual([]); }); diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts index 6af8450f5a2e8..3af6b8b29768d 100644 --- a/packages/kbn-telemetry-tools/src/tools/ts_parser.ts +++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts @@ -178,13 +178,11 @@ export function sourceHasUsageCollector(sourceFile: ts.SourceFile) { } const identifiers = (sourceFile as any).identifiers; - if ( - (!identifiers.get('makeUsageCollector') && !identifiers.get('type')) || - !identifiers.get('fetch') - ) { - return false; + if (identifiers.get('makeUsageCollector')) { + return true; } + return false; return true; } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 4e86ec4bd72e0..8422c34c9ed08 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@babel/cli": "^7.10.5", + "@jest/types": "^26.5.2", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/utils": "1.0.0", @@ -22,14 +23,18 @@ "diff": "^4.0.1" }, "dependencies": { + "@jest/reporters": "^26.5.2", "chalk": "^4.1.0", "dedent": "^0.7.0", "del": "^5.1.0", + "execa": "^4.0.2", "exit-hook": "^2.2.0", "getopts": "^2.2.5", "glob": "^7.1.2", + "globby": "^8.0.1", "joi": "^13.5.2", "lodash": "^4.17.20", + "mustache": "^2.3.2", "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^6.0.0", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 50ef521a2d811..a57b92fbdde25 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -55,8 +55,8 @@ export { export { runFailedTestsReporterCli } from './failed_tests_reporter'; -export { makeJunitReportPath } from './junit_report_path'; - export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export * from './functional_test_runner'; + +export * from './jest'; diff --git a/src/plugins/timelion/public/flot/index.js b/packages/kbn-test/src/jest/index.ts similarity index 78% rename from src/plugins/timelion/public/flot/index.js rename to packages/kbn-test/src/jest/index.ts index a066fd3ab8607..c6d680863d9c4 100644 --- a/src/plugins/timelion/public/flot/index.js +++ b/packages/kbn-test/src/jest/index.ts @@ -17,10 +17,6 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +export * from './junit_reporter'; + +export * from './report_path'; diff --git a/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js new file mode 100644 index 0000000000000..50016c883d378 --- /dev/null +++ b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js @@ -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. + */ + +const { resolve } = require('path'); +const { REPO_ROOT } = require('@kbn/utils'); + +module.exports = { + reporters: [ + 'default', + [ + `${REPO_ROOT}/packages/kbn-test/target/jest/junit_reporter`, + { + reportName: 'JUnit Reporter Integration Test', + rootDirectory: resolve( + REPO_ROOT, + 'packages/kbn-test/src/jest/integration_tests/__fixtures__' + ), + }, + ], + ], +}; diff --git a/src/dev/jest/integration_tests/__fixtures__/test.js b/packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js similarity index 89% rename from src/dev/jest/integration_tests/__fixtures__/test.js rename to packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js index fe0c65fd45b74..161012fb3a91c 100644 --- a/src/dev/jest/integration_tests/__fixtures__/test.js +++ b/packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js @@ -17,6 +17,8 @@ * under the License. */ -it('fails', () => { - throw new Error('failure'); +describe('JUnit Reporter', () => { + it('fails', () => { + throw new Error('failure'); + }); }); diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts similarity index 82% rename from src/dev/jest/integration_tests/junit_reporter.test.js rename to packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 3280482a54203..1390c44d84a07 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -25,27 +25,26 @@ import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; import { makeJunitReportPath } from '@kbn/test'; +import { REPO_ROOT } from '@kbn/utils'; const MINUTE = 1000 * 60; -const ROOT_DIR = resolve(__dirname, '../../../../'); const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'Jest Tests'); +const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); afterAll(async () => { await del(TARGET_DIR); }); const parseXml = promisify(xml2js.parseString); - it( 'produces a valid junit report for failures', async () => { const result = await execa( - process.execPath, - ['-r', require.resolve('../../../setup_node_env'), require.resolve('jest/bin/jest')], + './node_modules/.bin/jest', + ['--config', 'packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js'], { - cwd: FIXTURE_DIR, + cwd: REPO_ROOT, env: { CI: 'true', }, @@ -57,6 +56,7 @@ it( await expect(parseXml(readFileSync(XML_PATH, 'utf8'))).resolves.toEqual({ testsuites: { $: { + failures: '1', name: 'jest', skipped: '0', tests: '1', @@ -67,7 +67,7 @@ it( { $: { failures: '1', - file: resolve(ROOT_DIR, 'src/dev/jest/integration_tests/__fixtures__/test.js'), + file: resolve(FIXTURE_DIR, './test.js'), name: 'test.js', skipped: '0', tests: '1', @@ -77,8 +77,8 @@ it( testcase: [ { $: { - classname: 'Jest Tests.·', - name: 'fails', + classname: 'JUnit Reporter Integration Test.·', + name: 'JUnit Reporter fails', time: expect.anything(), }, failure: [expect.stringMatching(/Error: failure\s+at /m)], diff --git a/src/dev/jest/junit_reporter.js b/packages/kbn-test/src/jest/junit_reporter.ts similarity index 73% rename from src/dev/jest/junit_reporter.js rename to packages/kbn-test/src/jest/junit_reporter.ts index f33358c081a06..0712584122e05 100644 --- a/src/dev/jest/junit_reporter.js +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -22,20 +22,32 @@ import { writeFileSync, mkdirSync } from 'fs'; import xmlBuilder from 'xmlbuilder'; -import { escapeCdata, makeJunitReportPath } from '@kbn/test'; +import { REPO_ROOT } from '@kbn/utils'; +import type { Config } from '@jest/types'; +import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; -const ROOT_DIR = dirname(require.resolve('../../../package.json')); +import { escapeCdata } from '../mocha/xml'; +import { makeJunitReportPath } from './report_path'; + +interface ReporterOptions { + reportName?: string; + rootDirectory?: string; +} /** * Jest reporter that produces JUnit report when running on CI * @class JestJUnitReporter */ -export default class JestJUnitReporter { - constructor(globalConfig, options = {}) { - const { reportName = 'Jest Tests', rootDirectory = ROOT_DIR } = options; - this._reportName = reportName; - this._rootDirectory = resolve(rootDirectory); +// eslint-disable-next-line import/no-default-export +export default class JestJUnitReporter extends BaseReporter { + private _reportName: string; + private _rootDirectory: string; + + constructor(globalConfig: Config.GlobalConfig, { rootDirectory, reportName }: ReporterOptions) { + super(); + this._reportName = reportName || 'Jest Tests'; + this._rootDirectory = rootDirectory ? resolve(rootDirectory) : REPO_ROOT; } /** @@ -44,7 +56,7 @@ export default class JestJUnitReporter { * @param {JestResults} results see https://facebook.github.io/jest/docs/en/configuration.html#testresultsprocessor-string * @return {undefined} */ - onRunComplete(contexts, results) { + onRunComplete(contexts: Set, results: AggregatedResult): void { if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } @@ -55,18 +67,19 @@ export default class JestJUnitReporter { 'testsuites', { encoding: 'utf-8' }, {}, - { skipNullAttributes: true } + { keepNullAttributes: false } ); - const msToIso = (ms) => (ms ? new Date(ms).toISOString().slice(0, -5) : undefined); - const msToSec = (ms) => (ms ? (ms / 1000).toFixed(3) : undefined); + const msToIso = (ms: number | null | undefined) => + ms ? new Date(ms).toISOString().slice(0, -5) : undefined; + const msToSec = (ms: number | null | undefined) => (ms ? (ms / 1000).toFixed(3) : undefined); root.att({ name: 'jest', timestamp: msToIso(results.startTime), time: msToSec(Date.now() - results.startTime), tests: results.numTotalTests, - failures: results.numFailingTests, + failures: results.numFailedTests, skipped: results.numPendingTests, }); diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/jest/report_path.ts similarity index 93% rename from packages/kbn-test/src/junit_report_path.ts rename to packages/kbn-test/src/jest/report_path.ts index 90405d7a89c02..fe122c349c193 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/jest/report_path.ts @@ -18,7 +18,7 @@ */ import { resolve } from 'path'; -import { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; +import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 966fb65406ac6..4700479941eed 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -22,6 +22,7 @@ require('./polyfills'); // must load before angular export const Jquery = require('jquery'); window.$ = window.jQuery = Jquery; +require('./flot_charts'); // stateful deps export const KbnI18n = require('@kbn/i18n'); @@ -50,8 +51,9 @@ export const ElasticEui = require('@elastic/eui'); export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); +export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); -import * as Theme from './theme.ts'; -export { Theme }; +// runtime deps which don't need to be copied across all bundles +export const TsLib = require('tslib'); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md b/packages/kbn-ui-shared-deps/flot_charts/API.md similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md rename to packages/kbn-ui-shared-deps/flot_charts/API.md diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js b/packages/kbn-ui-shared-deps/flot_charts/index.js similarity index 52% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js rename to packages/kbn-ui-shared-deps/flot_charts/index.js index 613939256cfc9..6d9872d3ec524 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js +++ b/packages/kbn-ui-shared-deps/flot_charts/index.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ /* @notice @@ -32,17 +45,15 @@ * THE SOFTWARE. */ -import $ from 'jquery'; -if (window) window.jQuery = $; -require('./jquery.flot'); -require('./jquery.flot.time'); -require('./jquery.flot.canvas'); -require('./jquery.flot.symbol'); -require('./jquery.flot.crosshair'); -require('./jquery.flot.selection'); -require('./jquery.flot.pie'); -require('./jquery.flot.stack'); -require('./jquery.flot.threshold'); -require('./jquery.flot.fillbetween'); -require('./jquery.flot.log'); -module.exports = $; +import './jquery_flot'; +import './jquery_flot_canvas'; +import './jquery_flot_time'; +import './jquery_flot_symbol'; +import './jquery_flot_crosshair'; +import './jquery_flot_selection'; +import './jquery_flot_pie'; +import './jquery_flot_stack'; +import './jquery_flot_threshold'; +import './jquery_flot_fillbetween'; +import './jquery_flot_log'; +import './jquery_flot_axislabels'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.colorhelpers.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_colorhelpers.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.colorhelpers.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_colorhelpers.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.axislabels.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_axislabels.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.axislabels.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_axislabels.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.canvas.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_canvas.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.canvas.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_canvas.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.categories.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_categories.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.categories.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_categories.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.crosshair.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_crosshair.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.crosshair.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_crosshair.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_errorbars.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_errorbars.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.fillbetween.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_fillbetween.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.fillbetween.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_fillbetween.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_image.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_image.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_log.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_log.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.navigate.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_navigate.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.navigate.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_navigate.js diff --git a/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js new file mode 100644 index 0000000000000..c1301a0659bda --- /dev/null +++ b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js @@ -0,0 +1,896 @@ +/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + series: { + pie: { + show: true/false + radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' + innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect + startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result + tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) + offset: { + top: integer value to move the pie up or down + left: integer value to move the pie left or right, or 'auto' + }, + stroke: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') + width: integer pixel width of the stroke + }, + label: { + show: true/false, or 'auto' + formatter: a user-defined function that modifies the text/style of the label text + radius: 0-1 for percentage of fullsize, or a specified pixel length + background: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') + opacity: 0-1 + }, + threshold: 0-1 for the percentage value at which to hide labels (if they're too small) + }, + combine: { + threshold: 0-1 for the percentage value at which to combine slices (if they're too small) + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined + label: any text value of what the combined slice should be labeled + } + highlight: { + opacity: 0-1 + } + } + } + +More detail and specific examples can be found in the included HTML file. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + let canvas = null; + let target = null; + let options = null; + let maxRadius = null; + let centerLeft = null; + let centerTop = null; + let processed = false; + let ctx = null; + + // interactive variables + + let highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function (plot, options) { + if (options.series.pie.show) { + options.grid.show = false; + + // set labels.show + + if (options.series.pie.label.show === 'auto') { + if (options.legend.show) { + options.series.pie.label.show = false; + } else { + options.series.pie.label.show = true; + } + } + + // set radius + + if (options.series.pie.radius === 'auto') { + if (options.series.pie.label.show) { + options.series.pie.radius = 3 / 4; + } else { + options.series.pie.radius = 1; + } + } + + // ensure sane tilt + + if (options.series.pie.tilt > 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + const options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind('mousemove').mousemove(onMouseMove); + } + + if (options.grid.clickable) { + eventHolder.unbind('click').click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function (plot, series, data, datapoints) { + const options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function (plot, octx) { + const options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function (plot, newCtx) { + const options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + let total = 0; + let combined = 0; + let numCombined = 0; + let color = options.series.pie.combine.color; + const newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (let i = 0; i < data.length; ++i) { + let value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if (Array.isArray(value) && value.length === 1) { + value = value[0]; + } + + if (Array.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (let i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (let i = 0; i < data.length; ++i) { + const value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (let i = 0; i < data.length; ++i) { + const value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { + /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: (value * Math.PI * 2) / total, + percent: value / (total / 100), + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: (combined * Math.PI * 2) / total, + percent: combined / (total / 100), + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + if (!target) { + return; + } // if no series were passed + + const canvasWidth = plot.getPlaceholder().width(); + const canvasHeight = plot.getPlaceholder().height(); + const legendWidth = target.children().filter('.legend').children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left === 'auto') { + if (options.legend.position.match('w')) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + const slices = plot.getData(); + let attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS); + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + const errorMessage = i18n.translate('flot.pie.unableToDrawLabelsInsideCanvasErrorMessage', { + defaultMessage: 'Could not draw pie with labels contained inside canvas', + }); + target.prepend( + `
${errorMessage}
` + ); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter('.pieLabel, .pieLabelBackground').remove(); + } + + function drawShadow() { + const shadowLeft = options.series.pie.shadow.left; + const shadowTop = options.series.pie.shadow.top; + const edge = 10; + const alpha = options.series.pie.shadow.alpha; + let radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + if ( + radius >= canvasWidth / 2 - shadowLeft || + radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || + radius <= edge + ) { + return; + } // shadow would be outside canvas, so don't draw it + + ctx.save(); + ctx.translate(shadowLeft, shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = '#000'; + + // center and rotate to starting position + + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (let i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + const startAngle = Math.PI * options.series.pie.startAngle; + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + let currentAngle = startAngle; + for (let i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (let i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else { + return true; + } + + function drawSlice(angle, color, fill) { + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = 'round'; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); + } // Center of the pie + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius, currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius, currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + let currentAngle = startAngle; + const radius = + options.series.pie.label.radius > 1 + ? options.series.pie.label.radius + : maxRadius * options.series.pie.label.radius; + + for (let i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + if (slice.data[0][1] === 0) { + return true; + } + + // format label text + + const lf = options.legend.labelFormatter; + let text; + const plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + const halfAngle = (startAngle + slice.angle + startAngle) / 2; + const x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + const y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + const html = + "" + + text + + ''; + target.append(html); + + const label = target.children('#pieLabel' + index); + const labelTop = y - label.height() / 2; + const labelLeft = x - label.width() / 2; + + label.css('top', labelTop); + label.css('left', labelLeft); + + // check to make sure that the label is not outside the canvas + + if ( + 0 - labelTop > 0 || + 0 - labelLeft > 0 || + canvasHeight - (labelTop + label.height()) < 0 || + canvasWidth - (labelLeft + label.width()) < 0 + ) { + return false; + } + + if (options.series.pie.label.background.opacity !== 0) { + // put in the transparent background separately to avoid blended labels and label boxes + + let c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + const pos = 'top:' + labelTop + 'px;left:' + labelLeft + 'px;'; + $( + "
" + ) + .css('opacity', options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + // subtract the center + + layer.save(); + const innerRadius = + options.series.pie.innerRadius > 1 + ? options.series.pie.innerRadius + : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + // TODO: Canvas forked flot here! + if (options.series.pie.stroke.width > 0) { + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + } + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + let c = false; + const l = poly.length; + let j = l - 1; + for (let i = -1; ++i < l; j = i) { + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || + (poly[j][1] <= pt[1] && pt[1] < poly[i][1])) && + pt[0] < + ((poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1])) / (poly[j][1] - poly[i][1]) + + poly[i][0] && + (c = !c); + } + return c; + } + + function findNearbySlice(mouseX, mouseY) { + const slices = plot.getData(); + const options = plot.getOptions(); + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + let x; + let y; + + for (let i = 0; i < slices.length; ++i) { + const s = slices[i]; + + if (s.pie.show) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i, + }; + } + } else { + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + const p1X = radius * Math.cos(s.startAngle); + const p1Y = radius * Math.sin(s.startAngle); + const p2X = radius * Math.cos(s.startAngle + s.angle / 4); + const p2Y = radius * Math.sin(s.startAngle + s.angle / 4); + const p3X = radius * Math.cos(s.startAngle + s.angle / 2); + const p3Y = radius * Math.sin(s.startAngle + s.angle / 2); + const p4X = radius * Math.cos(s.startAngle + s.angle / 1.5); + const p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5); + const p5X = radius * Math.cos(s.startAngle + s.angle); + const p5Y = radius * Math.sin(s.startAngle + s.angle); + const arrPoly = [ + [0, 0], + [p1X, p1Y], + [p2X, p2Y], + [p3X, p3Y], + [p4X, p4Y], + [p5X, p5Y], + ]; + const arrPoint = [x, y]; + + // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i, + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent('plothover', e); + } + + function onClick(e) { + triggerClickHoverEvent('plotclick', e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + const offset = plot.offset(); + const canvasX = parseInt(e.pageX - offset.left, 10); + const canvasY = parseInt(e.pageY - offset.top, 10); + const item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + // clear auto-highlights + + for (let i = 0; i < highlights.length; ++i) { + const h = highlights[i]; + if (h.auto === eventname && !(item && h.series === item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + const pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + const i = indexOfHighlight(s); + + if (i === -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + const i = indexOfHighlight(s); + + if (i !== -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (let i = 0; i < highlights.length; ++i) { + const h = highlights[i]; + if (h.series === s) { + return i; + } + } + return -1; + } + + function drawOverlay(plot, octx) { + const options = plot.getOptions(); + + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (let i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = 'rgba(255, 255, 255, ' + options.series.pie.highlight.opacity + ')'; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); + } // Center of the pie + + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc( + 0, + 0, + radius, + series.startAngle + series.angle / 2, + series.startAngle + series.angle, + false + ); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + const options = { + series: { + pie: { + show: false, + radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0 /* for donut */, + startAngle: 3 / 2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02, // shadow alpha + }, + offset: { + top: 0, + left: 'auto', + }, + stroke: { + color: '#fff', + width: 1, + }, + label: { + show: 'auto', + formatter: function (label, slice) { + return ( + "
" + + label + + '
' + + Math.round(slice.percent) + + '%
' + ); + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0, + }, + threshold: 0, // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: 'Other', // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5, + }, + }, + }, + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.resize.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_resize.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.resize.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_resize.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.selection.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_selection.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.selection.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_selection.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.stack.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_stack.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.stack.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_stack.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.symbol.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_symbol.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.symbol.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_symbol.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.threshold.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_threshold.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.threshold.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_threshold.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js similarity index 92% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js index 991e87d364e8a..767088d1410e2 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js +++ b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js @@ -49,47 +49,47 @@ import { i18n } from '@kbn/i18n'; if (monthNames == null) { monthNames = [ - i18n.translate('xpack.monitoring.janLabel', { + i18n.translate('flot.time.janLabel', { defaultMessage: 'Jan', - }), i18n.translate('xpack.monitoring.febLabel', { + }), i18n.translate('flot.time.febLabel', { defaultMessage: 'Feb', - }), i18n.translate('xpack.monitoring.marLabel', { + }), i18n.translate('flot.time.marLabel', { defaultMessage: 'Mar', - }), i18n.translate('xpack.monitoring.aprLabel', { + }), i18n.translate('flot.time.aprLabel', { defaultMessage: 'Apr', - }), i18n.translate('xpack.monitoring.mayLabel', { + }), i18n.translate('flot.time.mayLabel', { defaultMessage: 'May', - }), i18n.translate('xpack.monitoring.junLabel', { + }), i18n.translate('flot.time.junLabel', { defaultMessage: 'Jun', - }), i18n.translate('xpack.monitoring.julLabel', { + }), i18n.translate('flot.time.julLabel', { defaultMessage: 'Jul', - }), i18n.translate('xpack.monitoring.augLabel', { + }), i18n.translate('flot.time.augLabel', { defaultMessage: 'Aug', - }), i18n.translate('xpack.monitoring.sepLabel', { + }), i18n.translate('flot.time.sepLabel', { defaultMessage: 'Sep', - }), i18n.translate('xpack.monitoring.octLabel', { + }), i18n.translate('flot.time.octLabel', { defaultMessage: 'Oct', - }), i18n.translate('xpack.monitoring.novLabel', { + }), i18n.translate('flot.time.novLabel', { defaultMessage: 'Nov', - }), i18n.translate('xpack.monitoring.decLabel', { + }), i18n.translate('flot.time.decLabel', { defaultMessage: 'Dec', })]; } if (dayNames == null) { - dayNames = [i18n.translate('xpack.monitoring.sunLabel', { + dayNames = [i18n.translate('flot.time.sunLabel', { defaultMessage: 'Sun', - }), i18n.translate('xpack.monitoring.monLabel', { + }), i18n.translate('flot.time.monLabel', { defaultMessage: 'Mon', - }), i18n.translate('xpack.monitoring.tueLabel', { + }), i18n.translate('flot.time.tueLabel', { defaultMessage: 'Tue', - }), i18n.translate('xpack.monitoring.wedLabel', { + }), i18n.translate('flot.time.wedLabel', { defaultMessage: 'Wed', - }), i18n.translate('xpack.monitoring.thuLabel', { + }), i18n.translate('flot.time.thuLabel', { defaultMessage: 'Thu', - }), i18n.translate('xpack.monitoring.friLabel', { + }), i18n.translate('flot.time.friLabel', { defaultMessage: 'Fri', - }), i18n.translate('xpack.monitoring.satLabel', { + }), i18n.translate('flot.time.satLabel', { defaultMessage: 'Sat', })]; } diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index a403ae63a8f70..8f931fae4f337 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -63,5 +63,10 @@ exports.externals = { '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', lodash: '__kbnSharedDeps__.Lodash', 'lodash/fp': '__kbnSharedDeps__.LodashFp', + + /** + * runtime deps which don't need to be copied across all bundles + */ + tslib: '__kbnSharedDeps__.TsLib', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index e5ebb874e58aa..0a154c537fec1 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -33,6 +33,7 @@ "rxjs": "^6.5.5", "styled-components": "^5.1.0", "symbol-observable": "^1.2.0", + "tslib": "^2.0.0", "whatwg-fetch": "^3.0.0" }, "devDependencies": { diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts new file mode 100644 index 0000000000000..f88cdd899ef81 --- /dev/null +++ b/src/core/public/apm_system.test.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('@elastic/apm-rum'); +import { init, apm } from '@elastic/apm-rum'; +import { ApmSystem } from './apm_system'; + +const initMock = init as jest.Mocked; +const apmMock = apm as DeeplyMockedKeys; + +describe('ApmSystem', () => { + afterEach(() => { + jest.resetAllMocks(); + jest.resetAllMocks(); + }); + + describe('setup', () => { + it('does not init apm if no config provided', async () => { + const apmSystem = new ApmSystem(undefined); + await apmSystem.setup(); + expect(initMock).not.toHaveBeenCalled(); + }); + + it('calls init with configuration', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(initMock).toHaveBeenCalledWith({ active: true }); + }); + + it('adds globalLabels if provided', async () => { + const apmSystem = new ApmSystem({ active: true, globalLabels: { alpha: 'one' } }); + await apmSystem.setup(); + expect(apm.addLabels).toHaveBeenCalledWith({ alpha: 'one' }); + }); + + describe('http request normalization', () => { + let windowSpy: any; + + beforeEach(() => { + windowSpy = jest.spyOn(global as any, 'window', 'get').mockImplementation(() => ({ + location: { + protocol: 'http:', + hostname: 'mykibanadomain.com', + port: '5601', + }, + })); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + it('adds an observe function', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(apm.observe).toHaveBeenCalledWith('transaction:end', expect.any(Function)); + }); + + /** + * Utility function to wrap functions that mutate their input but don't return the mutated value. + * Makes expects easier below. + */ + const returnArg = (func: (input: T) => any): ((input: T) => T) => { + return (input) => { + func(input); + return input; + }; + }; + + it('removes the hostname, port, and protocol only when all match window.location', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + // Strips the hostname, protocol, and port from URLs that are on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /asdf/qwerty' }); + + // Does not modify URLs that are not on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + }); + }); + + it('strips the basePath', async () => { + const apmSystem = new ApmSystem({ active: true }, '/alpha'); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + + // Works with relative URLs as well + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET /alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + }); + }); + }); +}); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 5e4953b96dc5b..3b3c1da01a925 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -17,14 +17,17 @@ * under the License. */ +import type { ApmBase } from '@elastic/apm-rum'; +import { modifyUrl } from '@kbn/std'; +import type { InternalApplicationStart } from './application'; + +/** "GET protocol://hostname:port/pathname" */ +const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OPTIONS|CONNECT|TRACE)\s(.*)$/; + /** * This is the entry point used to boot the frontend when serving a application * that lives in the Kibana Platform. - * - * Any changes to this file should be kept in sync with - * src/legacy/ui/ui_bundles/app_entry_template.js */ -import type { InternalApplicationStart } from './application'; interface ApmConfig { // AgentConfigOptions is not exported from @elastic/apm-rum @@ -42,7 +45,7 @@ export class ApmSystem { * `apmConfig` would be populated with relevant APM RUM agent * configuration if server is started with elastic.apm.* config. */ - constructor(private readonly apmConfig?: ApmConfig) { + constructor(private readonly apmConfig?: ApmConfig, private readonly basePath = '') { this.enabled = apmConfig != null && !!apmConfig.active; } @@ -54,6 +57,8 @@ export class ApmSystem { apm.addLabels(globalLabels); } + this.addHttpRequestNormalization(apm); + init(apmConfig); } @@ -73,4 +78,52 @@ export class ApmSystem { } }); } + + /** + * Adds an observer to the APM configuration for normalizing transactions of the 'http-request' type to remove the + * hostname, protocol, port, and base path. Allows for coorelating data cross different deployments. + */ + private addHttpRequestNormalization(apm: ApmBase) { + apm.observe('transaction:end', (t) => { + if (t.type !== 'http-request') { + return; + } + + /** Split URLs of the from "GET protocol://hostname:port/pathname" into method & hostname */ + const matches = t.name.match(HTTP_REQUEST_TRANSACTION_NAME_REGEX); + if (!matches) { + return; + } + + const [, method, originalUrl] = matches; + // Normalize the URL + const normalizedUrl = modifyUrl(originalUrl, (parts) => { + const isAbsolute = parts.hostname && parts.protocol && parts.port; + // If the request was to a different host, port, or protocol then don't change anything + if ( + isAbsolute && + (parts.hostname !== window.location.hostname || + parts.protocol !== window.location.protocol || + parts.port !== window.location.port) + ) { + return; + } + + // Strip the protocol, hostnname, port, and protocol slashes to normalize + parts.protocol = null; + parts.hostname = null; + parts.port = null; + parts.slashes = false; + + // Replace the basePath if present in the pathname + if (parts.pathname === this.basePath) { + parts.pathname = '/'; + } else if (parts.pathname?.startsWith(`${this.basePath}/`)) { + parts.pathname = parts.pathname?.slice(this.basePath.length); + } + }); + + t.name = `${method} ${normalizedUrl}`; + }); + } } diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index a083196004cf4..4536826a4a267 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -28,7 +28,7 @@ export async function __kbnBootstrap__() { ); let i18nError: Error | undefined; - const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig); + const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig, injectedMetadata.basePath); await Promise.all([ // eslint-disable-next-line no-console diff --git a/src/core/public/overlays/banners/banners_list.test.tsx b/src/core/public/overlays/banners/banners_list.test.tsx index dbee20790fa94..3850a88699d90 100644 --- a/src/core/public/overlays/banners/banners_list.test.tsx +++ b/src/core/public/overlays/banners/banners_list.test.tsx @@ -43,7 +43,7 @@ describe('BannersList', () => { ]); expect(mount().html()).toMatchInlineSnapshot( - `"

Hello!

"` + `"

Hello!

"` ); }); @@ -85,7 +85,7 @@ describe('BannersList', () => { // Two new banners should be rendered expect(component.html()).toMatchInlineSnapshot( - `"

First Banner!

Second banner!

"` + `"

First Banner!

Second banner!

"` ); // Original banner should be unmounted expect(unmount).toHaveBeenCalled(); diff --git a/src/core/public/overlays/banners/banners_list.tsx b/src/core/public/overlays/banners/banners_list.tsx index 6503af985f9c8..30acf6abff70b 100644 --- a/src/core/public/overlays/banners/banners_list.tsx +++ b/src/core/public/overlays/banners/banners_list.tsx @@ -59,6 +59,11 @@ const BannerItem: React.FunctionComponent<{ banner: OverlayBanner }> = ({ banner useEffect(() => banner.mount(element.current!), [banner]); // Only unmount / remount if banner object changed. return ( -
+
); }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 86a02d74dea15..d6a4224d9fab0 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -36,7 +36,33 @@ function generator({ # set -euo pipefail - docker pull ${baseOSImage} + retry_docker_pull() { + image=$1 + attempt=0 + max_retries=5 + + while true + do + attempt=$((attempt+1)) + + if [ $attempt -gt $max_retries ] + then + echo "Docker pull retries exceeded, aborting." + exit 1 + fi + + if docker pull "$image" + then + echo "Docker pull successful." + break + else + echo "Docker pull unsuccessful, attempt '$attempt'." + fi + + done + } + + retry_docker_pull ${baseOSImage} echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; diff --git a/src/dev/jest/config.integration.js b/src/dev/jest/config.integration.js index 2fd6bd120d553..970c00bb68b98 100644 --- a/src/dev/jest/config.integration.js +++ b/src/dev/jest/config.integration.js @@ -31,7 +31,10 @@ export default { ), reporters: [ 'default', - ['/src/dev/jest/junit_reporter.js', { reportName: 'Jest Integration Tests' }], + [ + '/packages/kbn-test/target/jest/junit_reporter', + { reportName: 'Jest Integration Tests' }, + ], ], setupFilesAfterEnv: ['/src/dev/jest/setup/after_env.integration.js'], }; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 3c556a4f1ba3c..f582a8f3d4410 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -99,5 +99,5 @@ export default { '/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts', '/node_modules/enzyme-to-json/serializer', ], - reporters: ['default', '/src/dev/jest/junit_reporter.js'], + reporters: ['default', '/packages/kbn-test/target/jest/junit_reporter'], }; diff --git a/src/dev/jest/integration_tests/__fixtures__/package.json b/src/dev/jest/integration_tests/__fixtures__/package.json deleted file mode 100644 index 1a9a446d524bc..0000000000000 --- a/src/dev/jest/integration_tests/__fixtures__/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "fixture", - "jest": { - "testMatch": [ - "**/test.js" - ], - "transform": { - "^.+\\.js$": "/../../babel_transform.js" - }, - "reporters": [ - ["/../../junit_reporter.js", {"rootDirectory": "."}] - ] - } -} diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 9f7eb9d9e1aa4..e362427682ec0 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -52,7 +52,7 @@ export async function generateNoticeFromSource({ productName, directory, log }: 'src/plugins/*/{node_modules,build,dist}/**', 'x-pack/{node_modules,build,dist,data}/**', 'x-pack/packages/*/{node_modules,build,dist}/**', - 'x-pack/plugins/*/{node_modules,build,dist}/**', + 'x-pack/plugins/**/{node_modules,build,dist}/**', '**/target/**', ], }; diff --git a/src/fixtures/telemetry_collectors/.telemetryrc.json b/src/fixtures/telemetry_collectors/.telemetryrc.json index 31203149c9b57..c25d31eb182c7 100644 --- a/src/fixtures/telemetry_collectors/.telemetryrc.json +++ b/src/fixtures/telemetry_collectors/.telemetryrc.json @@ -2,6 +2,7 @@ "root": ".", "output": ".", "exclude": [ - "./unmapped_collector.ts" + "./unmapped_collector.ts", + "./externally_defined_usage_collector/index.ts" ] } diff --git a/src/fixtures/telemetry_collectors/externally_defined_usage_collector/get_usage_collector.ts b/src/fixtures/telemetry_collectors/externally_defined_usage_collector/get_usage_collector.ts new file mode 100644 index 0000000000000..d158afe271ff4 --- /dev/null +++ b/src/fixtures/telemetry_collectors/externally_defined_usage_collector/get_usage_collector.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. + */ + +export interface Usage { + collectorName: string; +} + +export function getUsageCollector(collectorName: string) { + return { + type: 'externally_defined_usage_collector', + isReady: () => true, + schema: { + collectorName: { + type: 'keyword' as 'keyword', + }, + }, + fetch(): Usage { + return { + collectorName, + }; + }, + }; +} diff --git a/src/fixtures/telemetry_collectors/externally_defined_usage_collector/index.ts b/src/fixtures/telemetry_collectors/externally_defined_usage_collector/index.ts new file mode 100644 index 0000000000000..b88ecfced7e8a --- /dev/null +++ b/src/fixtures/telemetry_collectors/externally_defined_usage_collector/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getUsageCollector } from './get_usage_collector'; + +export function registerCollector(collectorSet: UsageCollectionSetup) { + const collectorName = 'some_configs'; + const collector = collectorSet.makeUsageCollector(getUsageCollector(collectorName)); + + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 755269d1a31be..3f7d05e8692c2 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -87,19 +87,19 @@ beforeEach(async () => { }); test('Add to library is compatible when embeddable on dashboard has value type input', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Add to library is not compatible when embeddable input is by reference', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Add to library is not compatible when view mode is set to view', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -120,7 +120,7 @@ test('Add to library is not compatible when embeddable is not in a dashboard con mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -128,7 +128,7 @@ test('Add to library replaces embeddableId but retains panel count', async () => const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -154,7 +154,7 @@ test('Add to library returns reference type input', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 3cc1a8a1dffe1..d89c38f297e8f 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -27,6 +27,7 @@ import { EmbeddableInput, isReferenceOrValueEmbeddable, } from '../../../../embeddable/public'; +import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; @@ -40,7 +41,7 @@ export class AddToLibraryAction implements ActionByType { coreStart = coreMock.createStart(); + unlinkAction = ({ + getDisplayName: () => 'unlink from dat library', + execute: jest.fn(), + } as unknown) as UnlinkFromLibraryAction; + const containerOptions = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, @@ -81,19 +88,19 @@ beforeEach(async () => { }); test('Notification is shown when embeddable on dashboard has reference type input', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Notification is not shown when embeddable input is by value', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Notification is not shown when view mode is set to view', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index bff0236c802f1..6a0b71d8250be 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -17,12 +17,13 @@ * under the License. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBadge } from '@elastic/eui'; +import React from 'react'; import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { reactToUiComponent } from '../../../../kibana_react/public'; +import { UnlinkFromLibraryAction } from '.'; +import { LibraryNotificationPopover } from './library_notification_popover'; export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; @@ -35,23 +36,32 @@ export class LibraryNotificationAction implements ActionByType ( - - {this.displayName} - - )); + private LibraryNotification: React.FC<{ context: LibraryNotificationActionContext }> = ({ + context, + }: { + context: LibraryNotificationActionContext; + }) => { + const { embeddable } = context; + return ( + + ); + }; + + public readonly MenuItem = reactToUiComponent(this.LibraryNotification); public getDisplayName({ embeddable }: LibraryNotificationActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { @@ -67,16 +77,6 @@ export class LibraryNotificationAction implements ActionByType { - if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { - throw new IncompatibleActionError(); - } - return i18n.translate('dashboard.panel.libraryNotification.toolTip', { - defaultMessage: - 'This panel is linked to a Library item. Editing the panel might affect other dashboards.', - }); - }; - public isCompatible = async ({ embeddable }: LibraryNotificationActionContext) => { return ( embeddable.getRoot().isContainer && diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx new file mode 100644 index 0000000000000..c6f223fa45c23 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { DashboardContainer } from '..'; +import { isErrorEmbeddable } from '../../embeddable_plugin'; +import { mountWithIntl } from '../../../../../test_utils/public/enzyme_helpers'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { getSampleDashboardInput } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../embeddable_plugin_test_samples'; +import { + LibraryNotificationPopover, + LibraryNotificationProps, +} from './library_notification_popover'; +import { CoreStart } from '../../../../../core/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EuiPopover } from '@elastic/eui'; + +describe('LibraryNotificationPopover', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) + ); + const start = doStart(); + + let container: DashboardContainer; + let defaultProps: LibraryNotificationProps; + let coreStart: CoreStart; + + beforeEach(async () => { + coreStart = coreMock.createStart(); + + const containerOptions = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + + container = new DashboardContainer(getSampleDashboardInput(), containerOptions); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + + defaultProps = { + unlinkAction: ({ + execute: jest.fn(), + getDisplayName: () => 'test unlink', + } as unknown) as LibraryNotificationProps['unlinkAction'], + displayName: 'test display', + context: { embeddable: contactCardEmbeddable }, + icon: 'testIcon', + id: 'testId', + }; + }); + + function mountComponent(props?: Partial) { + return mountWithIntl(); + } + + test('click library notification badge should open and close popover', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + let popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(true); + btn.simulate('click'); + popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(false); + }); + + test('popover should contain button with unlink action display name', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + const popover = component.find(EuiPopover); + const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton'); + expect(unlinkButton.text()).toEqual('test unlink'); + }); + + test('clicking unlink executes unlink action', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + const popover = component.find(EuiPopover); + const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton'); + unlinkButton.simulate('click'); + expect(defaultProps.unlinkAction.execute).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx new file mode 100644 index 0000000000000..8bc81b3296c3d --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.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, { useState } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LibraryNotificationActionContext, UnlinkFromLibraryAction } from '.'; + +export interface LibraryNotificationProps { + context: LibraryNotificationActionContext; + unlinkAction: UnlinkFromLibraryAction; + displayName: string; + icon: string; + id: string; +} + +export function LibraryNotificationPopover({ + unlinkAction, + displayName, + context, + icon, + id, +}: LibraryNotificationProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { embeddable } = context; + + return ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="upCenter" + > + {displayName} +
+ +

+ {i18n.translate('dashboard.panel.libraryNotification.toolTip', { + defaultMessage: + 'This panel is linked to a library item. Editing the panel might affect other dashboards.', + })} +

+
+
+ + + + unlinkAction.execute({ embeddable })} + > + {unlinkAction.getDisplayName({ embeddable })} + + + + +
+ ); +} diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index b4178fd40c768..0f61a74cd7036 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -81,19 +81,19 @@ beforeEach(async () => { }); test('Unlink is compatible when embeddable on dashboard has reference type input', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Unlink is not compatible when embeddable input is by value', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Unlink is not compatible when view mode is set to view', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -114,7 +114,7 @@ test('Unlink is not compatible when embeddable is not in a dashboard container', mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -122,7 +122,7 @@ test('Unlink replaces embeddableId but retains panel count', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -152,7 +152,7 @@ test('Unlink unwraps all attributes from savedObject', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index e2a6ec7dd3947..f5cf8b4e866a8 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -27,6 +27,7 @@ import { EmbeddableInput, isReferenceOrValueEmbeddable, } from '../../../../embeddable/public'; +import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary'; @@ -40,14 +41,14 @@ export class UnlinkFromLibraryAction implements ActionByType; -export { mockAttributeService } from './attribute_service/attribute_service.mock'; const createStartContract = (): DashboardStart => { // @ts-ignore - const startContract: DashboardStart = { - getAttributeService: jest.fn(), - }; + const startContract: DashboardStart = {}; return startContract; }; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 3325d193e56ed..574d456c10a8d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -39,8 +39,6 @@ import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart, - SavedObjectEmbeddableInput, - EmbeddableInput, PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; @@ -53,7 +51,6 @@ import { getSavedObjectFinder, SavedObjectLoader, SavedObjectsStart, - showSaveModal, } from '../../saved_objects/public'; import { ExitFullScreenButton as ExitFullScreenButtonUi, @@ -103,11 +100,6 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; -import { AttributeService } from '.'; -import { - AttributeServiceOptions, - ATTRIBUTE_SERVICE_KEY, -} from './attribute_service/attribute_service'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -156,16 +148,6 @@ export interface DashboardStart { dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; - getAttributeService: < - A extends { title: string }, - V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { - [ATTRIBUTE_SERVICE_KEY]: A; - }, - R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput - >( - type: string, - options: AttributeServiceOptions
- ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { @@ -430,11 +412,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { - uiActions, - data: { indexPatterns, search }, - embeddable, - } = plugins; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -452,24 +430,22 @@ export class DashboardPlugin uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction(); + const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); - const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); + + const unlinkFromLibraryAction = new UnlinkFromLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); - const libraryNotificationAction = new LibraryNotificationAction(); + const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns, - search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE @@ -483,15 +459,6 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), - getAttributeService: (type: string, options) => - new AttributeService( - type, - showSaveModal, - core.i18n.Context, - core.notifications.toasts, - options, - embeddable.getEmbeddableFactory - ), }; } diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index f3bdfd8e17f0a..bfc52ec33c35c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -16,11 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; @@ -45,10 +41,9 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - services: SavedObjectKibanaServices + savedObjectStart: SavedObjectsStart ): new (id: string) => SavedObjectDashboard { - const SavedObjectClass = createSavedObjectClass(services); - class SavedDashboard extends SavedObjectClass { + class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type public static type = 'dashboard'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 3bd4d66a693b1..750fec4d4d1f9 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -17,23 +17,19 @@ * under the License. */ -import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; -import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; - indexPatterns: IndexPatternsContract; - search: DataPublicPluginStart['search']; - chrome: ChromeStart; - overlays: OverlayStart; + savedObjects: SavedObjectsStart; } /** * @param services */ -export function createSavedDashboardLoader(services: Services) { - const SavedDashboard = createSavedDashboardClass(services); - return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient); +export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects); + return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index 850b2470dd475..a85f67f5ba56a 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -46,10 +46,10 @@ export const dashboardSavedObjectType: SavedObjectsType = { description: { type: 'text' }, hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { - properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + properties: { searchSourceJSON: { type: 'text', index: false } }, }, - optionsJSON: { type: 'text', index: false, doc_values: false }, - panelsJSON: { type: 'text', index: false, doc_values: false }, + optionsJSON: { type: 'text', index: false }, + panelsJSON: { type: 'text', index: false }, refreshInterval: { properties: { display: { type: 'keyword', index: false, doc_values: false }, diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.ts b/src/plugins/data/common/es_query/filters/get_filter_params.ts index 2e90ff0fe0691..040bb5b70f7a0 100644 --- a/src/plugins/data/common/es_query/filters/get_filter_params.ts +++ b/src/plugins/data/common/es_query/filters/get_filter_params.ts @@ -26,9 +26,10 @@ export function getFilterParams(filter: Filter) { case FILTERS.PHRASES: return (filter as PhrasesFilter).meta.params; case FILTERS.RANGE: + const { gte, gt, lte, lt } = (filter as RangeFilter).meta.params; return { - from: (filter as RangeFilter).meta.params.gte, - to: (filter as RangeFilter).meta.params.lt, + from: gte ?? gt, + to: lt ?? lte, }; } } diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 153b6a633b66d..2d6637daf4324 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -20,7 +20,6 @@ export * from './constants'; export * from './es_query'; export * from './field_formats'; -export * from './field_mapping'; export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index a8d53223c06d1..6e11bc8f1d508 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -34,34 +34,6 @@ class MockFieldFormatter {} fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; -jest.mock('../../field_mapping', () => { - const originalModule = jest.requireActual('../../field_mapping'); - - return { - ...originalModule, - expandShorthand: jest.fn(() => ({ - id: true, - title: true, - fieldFormatMap: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => []), - }, - fields: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation((fields) => fields), - }, - sourceFilters: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => undefined), - }, - typeMeta: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => undefined), - }, - })), - }; -}); - // helper function to create index patterns function create(id: string) { const { diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 201e9f1ec402c..910c79f5dd0d7 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -400,6 +400,15 @@ export class AggConfig { return this.params.field; } + /** + * Returns the bucket path containing the main value the agg will produce + * (e.g. for sum of bytes it will point to the sum, for median it will point + * to the 50 percentile in the percentile multi value bucket) + */ + getValueBucketPath() { + return this.type.getValueBucketPath(this); + } + makeLabel(percentageMode = false) { if (this.params.customLabel) { return this.params.customLabel; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 1e3839038b0f7..3ffac0c12eb22 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -60,6 +60,7 @@ export interface AggTypeConfig< getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; + getValueBucketPath?: (agg: TAggConfig) => string; } // TODO need to make a more explicit interface for this @@ -210,6 +211,10 @@ export class AggType< return this.params.find((p: TParam) => p.name === name); }; + getValueBucketPath = (agg: TAggConfig) => { + return agg.id; + }; + /** * Generic AggType Constructor * @@ -233,6 +238,10 @@ export class AggType< this.createFilter = config.createFilter; } + if (config.getValueBucketPath) { + this.getValueBucketPath = config.getValueBucketPath; + } + if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index b53ae44c05075..ead88f924731b 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -68,6 +68,7 @@ describe('AggConfig Filters', () => { { gte: 1024, lt: 2048.0, + label: 'A custom label', } ); @@ -78,6 +79,7 @@ describe('AggConfig Filters', () => { expect(filter.range).toHaveProperty('bytes'); expect(filter.range.bytes).toHaveProperty('gte', 1024.0); expect(filter.range.bytes).toHaveProperty('lt', 2048.0); + expect(filter.range.bytes).not.toHaveProperty('label'); expect(filter.meta).toHaveProperty('formattedValue'); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts index 8dea33a450c5d..bea8e577b21fb 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts @@ -25,7 +25,7 @@ import { IBucketAggConfig } from '../bucket_agg_type'; export const createFilterRange = ( getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart'] ) => { - return (aggConfig: IBucketAggConfig, params: any) => { + return (aggConfig: IBucketAggConfig, { label, ...params }: any) => { const { deserialize } = getFieldFormatsStart(); return buildRangeFilter( aggConfig.params.field, diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index 04e64233ce196..8128f1a18a66a 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -69,6 +69,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a string', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('20 minutes'); @@ -77,6 +78,23 @@ describe('TimeBuckets', () => { expect(interval.expression).toEqual('20m'); }); + test('getInterval - should scale interval', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('1m'); + + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('day'); + expect(interval.esValue).toEqual(1); + expect(interval.esUnit).toEqual('d'); + expect(interval.expression).toEqual('1d'); + }); + test('setInterval/getInterval - intreval is a string and bounds is defined', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); const bounds = { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index d054df0c9274e..f11f89317aea6 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -263,18 +263,16 @@ export class TimeBuckets { } const maxLength: number = this._timeBucketConfig['histogram:maxBars']; - const approxLen = Number(duration) / Number(interval); + const minInterval = calcAutoIntervalLessThan(maxLength, Number(duration)); let scaled; - if (approxLen > maxLength) { - scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); + if (interval < minInterval) { + scaled = minInterval; } else { return interval; } - if (+scaled === +interval) return interval; - interval = decorateInterval(interval); return Object.assign(scaled, { preScaled: interval, diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index 169b234845274..bdb6ea7cd4b98 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -41,6 +41,7 @@ export interface AggParamsRange extends BaseAggParams { ranges?: Array<{ from: number; to: number; + label?: string; }>; } @@ -71,7 +72,7 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend key = keys.get(id); if (!key) { - key = new RangeKey(bucket); + key = new RangeKey(bucket, agg.params.ranges); keys.set(id, key); } @@ -102,7 +103,11 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend { from: 1000, to: 2000 }, ], write(aggConfig, output) { - output.params.ranges = aggConfig.params.ranges; + output.params.ranges = (aggConfig.params as AggParamsRange).ranges?.map((range) => ({ + to: range.to, + from: range.from, + })); + output.params.keyed = true; }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/range_key.ts b/src/plugins/data/common/search/aggs/buckets/range_key.ts index cd781f7e082a2..43fdc20e53f55 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_key.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_key.ts @@ -19,14 +19,36 @@ const id = Symbol('id'); +type Ranges = Array< + Partial<{ + from: string | number; + to: string | number; + label: string; + }> +>; + export class RangeKey { [id]: string; gte: string | number; lt: string | number; + label?: string; + + private findCustomLabel( + from: string | number | undefined | null, + to: string | number | undefined | null, + ranges?: Ranges + ) { + return (ranges || []).find( + (range) => + ((from == null && range.from == null) || range.from === from) && + ((to == null && range.to == null) || range.to === to) + )?.label; + } - constructor(bucket: any) { + constructor(bucket: any, allRanges?: Ranges) { this.gte = bucket.from == null ? -Infinity : bucket.from; this.lt = bucket.to == null ? +Infinity : bucket.to; + this.label = this.findCustomLabel(bucket.from, bucket.to, allRanges); this[id] = RangeKey.idBucket(bucket); } diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 2c5be00c8afea..8f645b4712c7f 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -18,6 +18,7 @@ */ import { AggConfigs } from '../agg_configs'; +import { METRIC_TYPES } from '../metrics'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -133,5 +134,49 @@ describe('Terms Agg', () => { expect(params.include).toStrictEqual([1.1, 2, 3.33]); expect(params.exclude).toStrictEqual([4, 5.555, 6]); }); + + test('uses correct bucket path for sorting by median', () => { + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + const field = { + name: 'field', + indexPattern, + }; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: 'test', + params: { + field: { + name: 'string_field', + type: 'string', + }, + orderAgg: { + type: METRIC_TYPES.MEDIAN, + params: { + field: { + name: 'number_field', + type: 'number', + }, + }, + }, + }, + type: BUCKET_TYPES.TERMS, + }, + ], + { typesRegistry: mockAggTypesRegistry() } + ); + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' }); + }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 1363d38748c8b..3d543e6c5f574 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -41,7 +41,6 @@ import { export const termsAggFilter = [ '!top_hits', '!percentiles', - '!median', '!std_dev', '!derivative', '!moving_avg', @@ -198,14 +197,14 @@ export const getTermsBucketAgg = () => return; } - const orderAggId = orderAgg.id; + const orderAggPath = orderAgg.getValueBucketPath(); if (orderAgg.parentId && aggs) { orderAgg = aggs.byId(orderAgg.parentId); } output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAggId] = dir; + order[orderAggPath] = dir; }, }, { diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index f3f2d157ebafc..42298586cb68f 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -63,6 +63,12 @@ describe('AggTypeMetricMedianProvider class', () => { expect(dsl.median.percentiles.percents).toEqual([50]); }); + it('points to right value within multi metric for value bucket path', () => { + expect(aggConfigs.byId(METRIC_TYPES.MEDIAN)!.getValueBucketPath()).toEqual( + `${METRIC_TYPES.MEDIAN}.50` + ); + }); + it('converts the response', () => { const agg = aggConfigs.getResponseAggs()[0]; diff --git a/src/plugins/data/common/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts index 7b48a664b5fb9..a189461020915 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.ts @@ -42,6 +42,9 @@ export const getMedianMetricAgg = () => { values: { field: aggConfig.getFieldDisplayName() }, }); }, + getValueBucketPath(aggConfig) { + return `${aggConfig.id}.50`; + }, params: [ { name: 'field', diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts index 20d8cfc105e49..28646c092c01c 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts @@ -79,6 +79,16 @@ describe('getFormatWithAggs', () => { expect(getFormat).toHaveBeenCalledTimes(1); }); + test('returns custom label for range if provided', () => { + const mapping = { id: 'range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: 1, lt: 20, label: 'custom' })).toBe('custom'); + // underlying formatter is not called because custom label can be used directly + expect(getFormat).toHaveBeenCalledTimes(0); + }); + test('creates custom format for terms', () => { const mapping = { id: 'terms', diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts index 01369206ab3cf..a8134619fec0d 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts @@ -48,6 +48,9 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma const customFormats: Record IFieldFormat> = { range: () => { const RangeFormat = FieldFormat.from((range: any) => { + if (range.label) { + return range.label; + } const nestedFormatter = params as SerializedFieldFormat; const format = getFieldFormat({ id: nestedFormatter.id, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 9d417684b1651..c041511745be2 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -463,8 +463,6 @@ export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; export { ACTION_GLOBAL_APPLY_FILTER, ApplyGlobalFilterActionContext } from './actions'; -export * from '../common/field_mapping'; - /* * Plugin setup */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 13f658b562d39..2ed3e440040de 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -128,6 +128,7 @@ export class AggConfig { getTimeRange(): import("../../../public").TimeRange | undefined; // (undocumented) getValue(bucket: any): any; + getValueBucketPath(): string; // (undocumented) id: string; // (undocumented) @@ -651,11 +652,6 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; -// Warning: (ae-forgotten-export) The symbol "ShorthandFieldMapObject" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export const expandShorthand: (sh: Record) => MappingObject; - // Warning: (ae-missing-release-tag) "extractReferences" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -782,16 +778,6 @@ export type FieldFormatsStart = Omit // @public (undocumented) export const fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList; -// @public (undocumented) -export interface FieldMappingSpec { - // (undocumented) - _deserialize?: (mapping: string) => any | undefined; - // (undocumented) - _serialize?: (mapping: any) => string | undefined; - // (undocumented) - type: ES_FIELD_TYPES; -} - // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1290,11 +1276,11 @@ export type IndexPatternsContract = PublicMethodsOf; // Warning: (ae-missing-release-tag) "IndexPatternSelectProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { +export type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, 'placeholder'> & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; - savedObjectsClient: SavedObjectsClientContract; }; // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1518,9 +1504,6 @@ export interface KueryNode { type: keyof NodeTypes; } -// @public (undocumented) -export type MappingObject = Record; - // Warning: (ae-missing-release-tag) "MatchAllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 1280664ac8389..6c1a4eff786f6 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -54,6 +54,10 @@ function clearNowTimeStub() { delete global.nowTime; } +test('isTimeTouched is initially set to false', () => { + expect(timefilter.isTimeTouched()).toBe(false); +}); + describe('setTime', () => { let update: sinon.SinonSpy; let fetch: sinon.SinonSpy; @@ -84,6 +88,10 @@ describe('setTime', () => { }); }); + test('should update isTimeTouched', () => { + expect(timefilter.isTimeTouched()).toBe(true); + }); + test('should not add unexpected object keys to time state', () => { const unexpectedKey = 'unexpectedKey'; timefilter.setTime({ diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 5eb1546fa015f..01b82087cf354 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -41,6 +41,8 @@ export class Timefilter { private fetch$ = new Subject(); private _time: TimeRange; + // Denotes whether setTime has been called, can be used to determine if the constructor defaults are being used. + private _isTimeTouched: boolean = false; private _refreshInterval!: RefreshInterval; private _history: TimeHistoryContract; @@ -68,6 +70,10 @@ export class Timefilter { return this._isAutoRefreshSelectorEnabled; } + public isTimeTouched() { + return this._isTimeTouched; + } + public getEnabledUpdated$ = () => { return this.enabledUpdated$.asObservable(); }; @@ -112,6 +118,7 @@ export class Timefilter { from: newTime.from, to: newTime.to, }; + this._isTimeTouched = true; this._history.add(this._time); this.timeUpdate$.next(); this.fetch$.next(); diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 7863000b1ace4..060257a880528 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -26,6 +26,7 @@ const createSetupContractMock = () => { const timefilterMock: jest.Mocked = { isAutoRefreshSelectorEnabled: jest.fn(), isTimeRangeSelectorEnabled: jest.fn(), + isTimeTouched: jest.fn(), getEnabledUpdated$: jest.fn(), getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx index a9ff0c3b38ae6..007689dd0269d 100644 --- a/src/plugins/data/public/search/errors/timeout_error.tsx +++ b/src/plugins/data/public/search/errors/timeout_error.tsx @@ -97,7 +97,12 @@ export class SearchTimeoutError extends KbnError { <> - this.onClick(application)} size="s"> + this.onClick(application)} + size="s" + data-test-subj="searchTimeoutError" + > {actionText} diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 2d582b30bcd14..734e88e085661 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -127,7 +127,7 @@ export class SearchService implements Plugin { request: SearchStrategyRequest, options: ISearchOptions ) => { - return search(request, options).toPromise() as Promise; + return search(request, options).toPromise(); }, onResponse: handleResponse, legacy: { diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 73ec14de82b43..b79b7c038f9cc 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -33,8 +33,6 @@ } .globalFilterItem-isError, .globalFilterItem-isWarning { - text-decoration: none; - .globalFilterLabel__value { font-weight: $euiFontWeightBold; } diff --git a/src/plugins/data/public/ui/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss index 3a9a0df4332c8..efe2e28ac3b8a 100644 --- a/src/plugins/data/public/ui/filter_bar/_variables.scss +++ b/src/plugins/data/public/ui/filter_bar/_variables.scss @@ -1,3 +1,4 @@ $kbnGlobalFilterItemBorderColor: tintOrShade($euiColorMediumShade, 35%, 20%); $kbnGlobalFilterItemBorderColorExcluded: tintOrShade($euiColorDanger, 70%, 50%); $kbnGlobalFilterItemPinnedColorExcluded: tintOrShade($euiColorDanger, 30%, 20%); +$kbnGlobalFilterItemEditorWidth: 420px; // if changing this make sure to also change `FILTER_EDITOR_WIDTH` in ./filter_item.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index fdd952e2207d9..0d544ac9ad16a 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -23,7 +23,7 @@ import classNames from 'classnames'; import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; -import { FilterItem } from './filter_item'; +import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; import { useKibana } from '../../../../kibana_react/public'; import { IIndexPattern } from '../..'; @@ -112,7 +112,7 @@ function FilterBarUI(props: Props) { repositionOnScroll > -
+
{ private renderRegularEditor() { return (
- + {this.renderFieldInput()} {this.renderOperatorInput()} diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index cbff20115f8ea..018f41ab82bfc 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -62,6 +62,13 @@ export type FilterLabelStatus = | typeof FILTER_ITEM_WARNING | typeof FILTER_ITEM_ERROR; +/** + * @remarks + * if changing this make sure to also change + * $kbnGlobalFilterItemEditorWidth + */ +export const FILTER_EDITOR_WIDTH = 420; + export function FilterItem(props: Props) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [indexPatternExists, setIndexPatternExists] = useState(undefined); @@ -228,7 +235,7 @@ export function FilterItem(props: Props) { }, { id: 1, - width: 420, + width: FILTER_EDITOR_WIDTH, content: (
) => ( + return (props: IndexPatternSelectProps) => ( ); } diff --git a/src/plugins/data/public/ui/index_pattern_select/index.tsx b/src/plugins/data/public/ui/index_pattern_select/index.tsx index 2912ec401b8b6..f0db37eb963fd 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; -import type { IndexPatternSelectProps } from './index_pattern_select'; +import type { IndexPatternSelectInternalProps } from './index_pattern_select'; const Fallback = () => ( @@ -28,7 +28,7 @@ const Fallback = () => ( ); const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); -export const IndexPatternSelect = (props: IndexPatternSelectProps) => ( +export const IndexPatternSelect = (props: IndexPatternSelectInternalProps) => ( }> diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 8de1e2c16f159..1e0e8934778ad 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -27,12 +27,19 @@ import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; import { getTitle } from '../../../common/index_patterns/lib'; export type IndexPatternSelectProps = Required< - Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, - 'onChange' | 'placeholder' + Omit< + EuiComboBoxProps, + 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange' + >, + 'placeholder' > & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; +}; + +export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { savedObjectsClient: SavedObjectsClientContract; }; @@ -60,11 +67,11 @@ const getIndexPatterns = async ( // Needed for React.lazy // eslint-disable-next-line import/no-default-export -export default class IndexPatternSelect extends Component { +export default class IndexPatternSelect extends Component { private isMounted: boolean = false; state: IndexPatternSelectState; - constructor(props: IndexPatternSelectProps) { + constructor(props: IndexPatternSelectInternalProps) { super(props); this.state = { @@ -86,7 +93,7 @@ export default class IndexPatternSelect extends Component { +// FLAKY: https://github.com/elastic/kibana/issues/79910 +describe.skip('SearchBar', () => { const SEARCH_BAR_TEST_ID = 'globalQueryBar'; const SEARCH_BAR_ROOT = '.globalQueryBar'; const FILTER_BAR = '.globalFilterBar'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 44699993dd135..d665e3715fa72 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CoreSetup, CoreStart, Plugin, KibanaRequest, Logger } from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin, Logger, SavedObjectsClientContract } from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; @@ -29,7 +29,7 @@ import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( - kibanaRequest: KibanaRequest + savedObjectsClient: SavedObjectsClientContract ) => Promise; } @@ -47,11 +47,10 @@ export class IndexPatternsService implements Plugin { - const savedObjectsClient = savedObjects.getScopedClient(kibanaRequest); + indexPatternsServiceFactory: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts index 35cee799ddb6a..1794df7391cb0 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts @@ -19,6 +19,8 @@ import { fetchProvider } from './fetch'; import { LegacyAPICaller } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; jest.mock('../../../common', () => ({ DEFAULT_QUERY_LANGUAGE: 'lucene', @@ -29,6 +31,8 @@ jest.mock('../../../common', () => ({ let fetch: ReturnType; let callCluster: LegacyAPICaller; +let collectorFetchContext: CollectorFetchContext; +const collectorFetchContextMock = createCollectorFetchContextMock(); function setupMockCallCluster( optCount: { optInCount?: number; optOutCount?: number } | null, @@ -89,40 +93,64 @@ describe('makeKQLUsageCollector', () => { it('should return opt in data from the .kibana/kql-telemetry doc', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(1); expect(fetchResponse.optOutCount).toBe(0); }); it('should return the default query language set in advanced settings', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('kuery'); }); // Indicates the user has modified the setting at some point but the value is currently the default it('should return the kibana default query language if the config value is null', async () => { setupMockCallCluster({ optInCount: 1 }, null); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('lucene'); }); it('should indicate when the default language has never been modified by the user', async () => { setupMockCallCluster({ optInCount: 1 }, undefined); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); it('should default to 0 opt in counts if the .kibana/kql-telemetry doc does not exist', async () => { setupMockCallCluster(null, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(0); expect(fetchResponse.optOutCount).toBe(0); }); it('should default to the kibana default language if the config document does not exist', async () => { setupMockCallCluster(null, 'missingConfigDoc'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); }); diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 109d6f812334d..21a1843d1ec81 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common'; const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE; @@ -30,7 +30,7 @@ export interface Usage { } export function fetchProvider(index: string) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const [response, config] = await Promise.all([ callCluster('get', { index, diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 3551767eab017..344bc18c7b4b6 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -19,7 +19,8 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { LegacyAPICaller, SharedGlobalConfig } from 'kibana/server'; +import { SharedGlobalConfig } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Usage } from './register'; interface SearchTelemetrySavedObject { @@ -27,7 +28,7 @@ interface SearchTelemetrySavedObject { } export function fetchProvider(config$: Observable) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); const response = await callCluster('search', { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 504ce728481f0..2dbcc3196aa75 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -35,7 +35,8 @@ describe('ES search strategy', () => { }, }, }); - const mockContext = { + + const mockContext = ({ core: { uiSettings: { client: { @@ -44,7 +45,8 @@ describe('ES search strategy', () => { }, elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } }, }, - }; + } as unknown) as RequestHandlerContext; + const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { @@ -57,44 +59,51 @@ describe('ES search strategy', () => { expect(typeof esSearch.search).toBe('function'); }); - it('calls the API caller with the params with defaults', async () => { + it('calls the API caller with the params with defaults', async (done) => { const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - ignore_unavailable: true, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + ignore_unavailable: true, + track_total_hits: true, + }); + done(); + }); }); - it('calls the API caller with overridden defaults', async () => { + it('calls the API caller with overridden defaults', async (done) => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + track_total_hits: true, + }); + done(); + }); }); - it('has all response parameters', async () => { - const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - const response = await esSearch.search((mockContext as unknown) as RequestHandlerContext, { - params, - }); - - expect(response.isRunning).toBe(false); - expect(response.isPartial).toBe(false); - expect(response).toHaveProperty('loaded'); - expect(response).toHaveProperty('rawResponse'); - }); + it('has all response parameters', async (done) => + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search( + { + params: { index: 'logstash-*' }, + }, + {}, + mockContext + ) + .subscribe((data) => { + expect(data.isRunning).toBe(false); + expect(data.isPartial).toBe(false); + expect(data).toHaveProperty('loaded'); + expect(data).toHaveProperty('rawResponse'); + done(); + })); }); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 6e185d30ad56a..92cc941e14853 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { Observable, from } from 'rxjs'; import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; import { toSnakeCase } from './to_snake_case'; @@ -29,6 +29,7 @@ import { getTotalLoaded, getShardTimeout, shimAbortSignal, + IEsSearchResponse, } from '..'; export const esSearchStrategyProvider = ( @@ -37,47 +38,52 @@ export const esSearchStrategyProvider = ( usage?: SearchUsage ): ISearchStrategy => { return { - search: async (context, request, options) => { - logger.debug(`search ${request.params?.index}`); - const config = await config$.pipe(first()).toPromise(); - const uiSettingsClient = await context.core.uiSettings.client; + search: (request, options, context) => + from( + new Promise(async (resolve, reject) => { + logger.debug(`search ${request.params?.index}`); + const config = await config$.pipe(first()).toPromise(); + const uiSettingsClient = await context.core.uiSettings.client; - // Only default index pattern type is supported here. - // See data_enhanced for other type support. - if (!!request.indexType) { - throw new Error(`Unsupported index pattern type ${request.indexType}`); - } + // Only default index pattern type is supported here. + // See data_enhanced for other type support. + if (!!request.indexType) { + throw new Error(`Unsupported index pattern type ${request.indexType}`); + } - // ignoreThrottled is not supported in OSS - const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams(uiSettingsClient); + // ignoreThrottled is not supported in OSS + const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams( + uiSettingsClient + ); - const params = toSnakeCase({ - ...defaultParams, - ...getShardTimeout(config), - ...request.params, - }); + const params = toSnakeCase({ + ...defaultParams, + ...getShardTimeout(config), + ...request.params, + }); - try { - const promise = shimAbortSignal( - context.core.elasticsearch.client.asCurrentUser.search(params), - options?.abortSignal - ); - const { body: rawResponse } = (await promise) as ApiResponse>; + try { + const promise = shimAbortSignal( + context.core.elasticsearch.client.asCurrentUser.search(params), + options?.abortSignal + ); + const { body: rawResponse } = (await promise) as ApiResponse>; - if (usage) usage.trackSuccess(rawResponse.took); + if (usage) usage.trackSuccess(rawResponse.took); - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { - isPartial: false, - isRunning: false, - rawResponse, - ...getTotalLoaded(rawResponse._shards), - }; - } catch (e) { - if (usage) usage.trackError(); - throw e; - } - }, + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + resolve({ + isPartial: false, + isRunning: false, + rawResponse, + ...getTotalLoaded(rawResponse._shards), + }); + } catch (e) { + if (usage) usage.trackError(); + reject(e); + } + }) + ), }; }; diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index d4404c318ab47..834e5de5c3121 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { Observable, from } from 'rxjs'; import { CoreSetup, @@ -66,7 +66,8 @@ describe('Search service', () => { }, }, }; - mockDataStart.search.search.mockResolvedValue(response); + + mockDataStart.search.search.mockReturnValue(from(Promise.resolve(response))); const mockContext = {}; const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; @@ -83,7 +84,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response, @@ -91,12 +92,16 @@ describe('Search service', () => { }); it('handler throws an error if the search throws an error', async () => { - mockDataStart.search.search.mockRejectedValue({ - message: 'oh no', - body: { - error: 'oops', - }, - }); + const rejectedValue = from( + Promise.reject({ + message: 'oh no', + body: { + error: 'oops', + }, + }) + ); + + mockDataStart.search.search.mockReturnValue(rejectedValue); const mockContext = {}; const mockBody = { id: undefined, params: {} }; @@ -114,7 +119,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 492ad4395b32a..1e8433d9685e3 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -49,14 +49,16 @@ export function registerSearchRoute( const [, , selfStart] = await getStartServices(); try { - const response = await selfStart.search.search( - context, - { ...searchRequest, id }, - { - abortSignal, - strategy, - } - ); + const response = await selfStart.search + .search( + { ...searchRequest, id }, + { + abortSignal, + strategy, + }, + context + ) + .toPromise(); return res.ok({ body: { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 1a9e7d83bc956..0130d3aacc91f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -49,10 +49,10 @@ import { IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, - ISearchOptions, SearchSourceDependencies, SearchSourceService, searchSourceRequiredUiSettings, + ISearchOptions, } from '../../common/search'; import { getShardDelayBucketAgg, @@ -151,18 +151,14 @@ export class SearchService implements Plugin { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), getSearchStrategy: this.getSearchStrategy, - search: ( - context: RequestHandlerContext, - searchRequest: IKibanaSearchRequest, - options: Record - ) => { - return this.search(context, searchRequest, options); - }, + search: this.search.bind(this), searchSource: { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); const savedObjectsClient = savedObjects.getScopedClient(request); - const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory(request); + const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient + ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); // cache ui settings, only including items which are explicitly needed by SearchSource @@ -173,7 +169,13 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: (key: string): T => uiSettingsCache[key], - search: (searchRequest, options) => { + search: < + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse + >( + searchStrategyRequest: SearchStrategyRequest, + options: ISearchOptions + ) => { /** * Unless we want all SearchSource users to provide both a KibanaRequest * (needed for index patterns) AND the RequestHandlerContext (needed for @@ -193,7 +195,12 @@ export class SearchService implements Plugin { }, }, } as RequestHandlerContext; - return this.search(fakeRequestHandlerContext, searchRequest, options); + + return this.search( + searchStrategyRequest, + options, + fakeRequestHandlerContext + ).toPromise(); }, // onResponse isn't used on the server, so we just return the original value onResponse: (req, res) => res, @@ -232,13 +239,15 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - context: RequestHandlerContext, searchRequest: SearchStrategyRequest, - options: ISearchOptions - ): Promise => { - return this.getSearchStrategy( + options: ISearchOptions, + context: RequestHandlerContext + ) => { + const strategy = this.getSearchStrategy( options.strategy || this.defaultSearchStrategyName - ).search(context, searchRequest, options); + ); + + return strategy.search(searchRequest, options, context); }; private getSearchStrategy = < diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 0de4ef529e896..9ba06d88dc4b3 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Observable } from 'rxjs'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ISearchOptions, @@ -57,6 +58,22 @@ export interface ISearchSetup { __enhance: (enhancements: SearchEnhancements) => void; } +/** + * Search strategy interface contains a search method that takes in a request and returns a promise + * that resolves to a response. + */ +export interface ISearchStrategy< + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse +> { + search: ( + request: SearchStrategyRequest, + options: ISearchOptions, + context: RequestHandlerContext + ) => Observable; + cancel?: (context: RequestHandlerContext, id: string) => Promise; +} + export interface ISearchStart< SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse @@ -69,28 +86,8 @@ export interface ISearchStart< getSearchStrategy: ( name: string ) => ISearchStrategy; - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options: ISearchOptions - ) => Promise; + search: ISearchStrategy['search']; searchSource: { asScoped: (request: KibanaRequest) => Promise; }; } - -/** - * Search strategy interface contains a search method that takes in a request and returns a promise - * that resolves to a response. - */ -export interface ISearchStrategy< - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse -> { - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options?: ISearchOptions - ) => Promise; - cancel?: (context: RequestHandlerContext, id: string) => Promise; -} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 45dbdee0f846b..0828460830f2c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -20,10 +20,10 @@ import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { ISavedObjectsRepository } from 'kibana/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { KibanaRequest } from 'src/core/server'; -import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; import { LegacyAPICaller } from 'kibana/server'; import { Logger } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; @@ -42,6 +42,7 @@ import { RequestHandlerContext } from 'src/core/server'; import { RequestStatistics } from 'src/plugins/inspector/common'; import { SavedObject } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; @@ -675,7 +676,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2) => Promise; }; } @@ -713,7 +714,7 @@ export interface ISearchStart ISearchStrategy; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; + search: ISearchStrategy['search']; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -727,7 +728,7 @@ export interface ISearchStrategy Promise; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; + search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; } // @public (undocumented) @@ -879,7 +880,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -1140,7 +1141,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:91:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 1a23f6deb5fa5..67c93ad8a406c 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -12,13 +12,9 @@ "urlForwarding", "navigation", "uiActions", - "visualizations" + "visualizations", + "savedObjects" ], "optionalPlugins": ["home", "share"], - "requiredBundles": [ - "kibanaUtils", - "home", - "savedObjects", - "kibanaReact" - ] + "requiredBundles": ["kibanaUtils", "home", "kibanaReact"] } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 3a0fed652a49a..615cfeb8c97a0 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -217,12 +217,7 @@ export function DiscoverLegacy({ /> )} {resultState === 'uninitialized' && } - {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} - -
- -
-
+ {resultState === 'loading' && } {resultState === 'ready' && (
diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx index 4e1754638d479..e3cc396783628 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function LoadingSpinner() { return ( - <> +

@@ -30,6 +30,6 @@ export function LoadingSpinner() { - +

); } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index fdb14b3f1f63e..27844cc2347b9 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -37,7 +37,6 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi import { SharePluginStart } from 'src/plugins/share/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { VisualizationsStart } from 'src/plugins/visualizations/public'; -import { SavedObjectKibanaServices } from 'src/plugins/saved_objects/public'; import { DiscoverStartPlugins } from './plugin'; import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; @@ -78,12 +77,9 @@ export async function buildServices( context: PluginInitializerContext, getEmbeddableInjector: any ): Promise { - const services: SavedObjectKibanaServices = { + const services = { savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }; const savedObjectService = createSavedSearchesLoader(services); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b1bbc89b62d9d..11ec4f08d9514 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -41,7 +41,7 @@ import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwardi import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; @@ -141,6 +141,7 @@ export interface DiscoverStartPlugins { urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; visualizations: VisualizationsStart; + savedObjects: SavedObjectsStart; } const innerAngularName = 'app/discover'; @@ -351,10 +352,7 @@ export class DiscoverPlugin urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }), }; } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 2b8574a8fa118..1ec4549f05d49 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -16,16 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../saved_objects/public'; -export function createSavedSearchClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSearch extends SavedObjectClass { +export function createSavedSearchClass(savedObjects: SavedObjectsStart) { + class SavedSearch extends savedObjects.SavedObjectClass { public static type: string = 'search'; public static mapping = { title: 'text', @@ -70,5 +64,5 @@ export function createSavedSearchClass(services: SavedObjectKibanaServices) { } } - return SavedSearch as new (id: string) => SavedObject; + return (SavedSearch as unknown) as new (id: string) => SavedObject; } diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/saved_searches.ts index 0bc332ed8ec74..fd7a185f7012f 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches.ts @@ -17,12 +17,18 @@ * under the License. */ -import { SavedObjectLoader, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; -export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { - const SavedSearchClass = createSavedSearchClass(services); - const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); +interface Services { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; +} + +export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) { + const SavedSearchClass = createSavedSearchClass(savedObjects); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index c13550e543ab6..a6e42f956a025 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -48,7 +48,7 @@ export const searchSavedObjectType: SavedObjectsType = { hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { - searchSourceJSON: { type: 'text', index: false, doc_values: false }, + searchSourceJSON: { type: 'text', index: false }, }, }, sort: { type: 'keyword', index: false, doc_values: false }, diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 7609f07d660bc..789353ca4abd7 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -77,6 +77,8 @@ export { EmbeddableRendererProps, } from './lib'; +export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service'; + export { EnhancementRegistryDefinition } from './types'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx similarity index 89% rename from src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx index 09d6f5b4f1e0d..9b08d52ed517c 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx @@ -17,11 +17,11 @@ * under the License. */ -import { EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddable_plugin'; -import { coreMock } from '../../../../core/public/mocks'; +import { EmbeddableInput, SavedObjectEmbeddableInput } from '../index'; +import { coreMock } from '../../../../../core/public/mocks'; import { AttributeServiceOptions } from './attribute_service'; -import { CoreStart } from '../../../../core/public'; -import { AttributeService, ATTRIBUTE_SERVICE_KEY } from '..'; +import { CoreStart } from 'src/core/public'; +import { AttributeService, ATTRIBUTE_SERVICE_KEY } from './index'; export const mockAttributeService = < A extends { title: string }, diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts similarity index 98% rename from src/plugins/dashboard/public/attribute_service/attribute_service.test.ts rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts index d7368b299c411..868501adb9687 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts @@ -19,8 +19,8 @@ import { ATTRIBUTE_SERVICE_KEY } from './attribute_service'; import { mockAttributeService } from './attribute_service.mock'; -import { coreMock } from '../../../../core/public/mocks'; -import { OnSaveProps } from '../../../saved_objects/public/save_modal'; +import { coreMock } from '../../../../../core/public/mocks'; +import { OnSaveProps } from 'src/plugins/saved_objects/public/save_modal'; interface TestAttributes { title: string; diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx similarity index 95% rename from src/plugins/dashboard/public/attribute_service/attribute_service.tsx rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx index b46226ec4ab02..c4628ab7fbdba 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx @@ -20,17 +20,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; +import { I18nStart, NotificationsStart } from 'src/core/public'; +import { SavedObjectSaveModal, OnSaveProps, SaveResult } from '../../../../saved_objects/public'; import { EmbeddableInput, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, IEmbeddable, Container, - EmbeddableStart, EmbeddableFactoryNotFoundError, -} from '../embeddable_plugin'; -import { I18nStart, NotificationsStart } from '../../../../core/public'; -import { SavedObjectSaveModal, OnSaveProps, SaveResult } from '../../../saved_objects/public'; + EmbeddableFactory, +} from '../index'; /** * The attribute service is a shared, generic service that embeddables can use to provide the functionality @@ -66,7 +66,7 @@ export class AttributeService< private i18nContext: I18nStart['Context'], private toasts: NotificationsStart['toasts'], private options: AttributeServiceOptions, - getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'] + getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory ) { if (getEmbeddableFactory) { const factory = getEmbeddableFactory(this.type); @@ -113,7 +113,7 @@ export class AttributeService< return { ...originalInput } as RefType; } catch (error) { this.toasts.addDanger({ - title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + title: i18n.translate('embeddableApi.attributeService.saveToLibraryError', { defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, values: { errorMessage: error.message, diff --git a/src/plugins/dashboard/public/attribute_service/index.ts b/src/plugins/embeddable/public/lib/attribute_service/index.ts similarity index 100% rename from src/plugins/dashboard/public/attribute_service/index.ts rename to src/plugins/embeddable/public/lib/attribute_service/index.ts diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx index 1e4604af9dc09..fa4d7f466caee 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx @@ -40,8 +40,10 @@ export function EmbeddableErrorLabel(props: Props) { return (
- - {labelText} + + + {labelText} +
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index a2da31773696c..137f8c24b1fae 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -20,6 +20,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elast import classNames from 'classnames'; import React from 'react'; import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -123,9 +124,11 @@ export class EmbeddablePanel extends React.Component { badges = badges.filter((badge) => disabledActions.indexOf(badge.id) === -1); } - this.setState({ - badges, - }); + if (!deepEqual(this.state.badges, badges)) { + this.setState({ + badges, + }); + } } private async refreshNotifications() { @@ -139,9 +142,11 @@ export class EmbeddablePanel extends React.Component { notifications = notifications.filter((badge) => disabledActions.indexOf(badge.id) === -1); } - this.setState({ - notifications, - }); + if (!deepEqual(this.state.notifications, notifications)) { + this.setState({ + notifications, + }); + } } public UNSAFE_componentWillMount() { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 7c4724a667433..9bcef051a9359 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -68,7 +68,13 @@ function renderNotifications( const context = { embeddable }; let badge = notification.MenuItem ? ( - React.createElement(uiToReactComponent(notification.MenuItem)) + React.createElement(uiToReactComponent(notification.MenuItem), { + key: notification.id, + context: { + embeddable, + trigger: panelNotificationTrigger, + }, + }) ) : ( ; export type Start = jest.Mocked; @@ -125,6 +126,7 @@ const createStartContract = (): Start => { EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), + getAttributeService: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 00eb923c26662..aa4d66c43c9db 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import { identity } from 'lodash'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; -import { getSavedObjectFinder } from '../../saved_objects/public'; +import { getSavedObjectFinder, showSaveModal } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; import { @@ -47,6 +47,7 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; @@ -56,6 +57,8 @@ import { telemetryBaseEmbeddableInput, } from '../common/lib/migrate_base_input'; import { PersistableState, SerializableState } from '../../kibana_utils/common'; +import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service'; +import { AttributeServiceOptions } from './lib/attribute_service/attribute_service'; export interface EmbeddableSetupDependencies { data: DataPublicPluginSetup; @@ -93,6 +96,16 @@ export interface EmbeddableStart extends PersistableState { EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; + getAttributeService: < + A extends { title: string }, + V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { + [ATTRIBUTE_SERVICE_KEY]: A; + }, + R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput + >( + type: string, + options: AttributeServiceOptions
+ ) => AttributeService; } export type EmbeddablePanelHOC = React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>; @@ -178,6 +191,15 @@ export class EmbeddablePublicPlugin implements Plugin + new AttributeService( + type, + showSaveModal, + core.i18n.Context, + core.notifications.toasts, + options, + this.getEmbeddableFactory + ), getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history, this.appList) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index b01995ccaab08..6280d3a2e4a50 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -31,6 +31,7 @@ import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; +import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { ISearchOptions } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; @@ -119,6 +120,42 @@ export class AddPanelAction implements Action_3 { readonly type = "ACTION_ADD_PANEL"; } +// Warning: (ae-missing-release-tag) "ATTRIBUTE_SERVICE_KEY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const ATTRIBUTE_SERVICE_KEY = "attributes"; + +// Warning: (ae-missing-release-tag) "AttributeService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AttributeService { + // Warning: (ae-forgotten-export) The symbol "AttributeServiceOptions" needs to be exported by the entry point index.d.ts + constructor(type: string, showSaveModal: (saveModal: React.ReactElement, I18nContext: I18nStart_2['Context']) => void, i18nContext: I18nStart_2['Context'], toasts: NotificationsStart_2['toasts'], options: AttributeServiceOptions, getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory); + // (undocumented) + getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType; + // (undocumented) + getInputAsRefType: (input: ValType | RefType, saveOptions?: { + showSaveModal: boolean; + saveModalTitle?: string | undefined; + } | { + title: string; + } | undefined) => Promise; + // (undocumented) + getInputAsValueType: (input: ValType | RefType) => Promise; + // (undocumented) + inputIsRefType: (input: ValType | RefType) => input is RefType; + // (undocumented) + unwrapAttributes(input: RefType | ValType): Promise; + // (undocumented) + wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise>; +} + // Warning: (ae-missing-release-tag) "ChartActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -527,6 +564,14 @@ export interface EmbeddableStart extends PersistableState { // (undocumented) EmbeddablePanel: EmbeddablePanelHOC; // (undocumented) + getAttributeService: (type: string, options: AttributeServiceOptions) => AttributeService; + // (undocumented) getEmbeddableFactories: () => IterableIterator; // (undocumented) getEmbeddableFactory: = IEmbeddable>(embeddableFactoryId: string) => EmbeddableFactory | undefined; diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index f79c4b7620110..1e93561e4d063 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -35,6 +35,7 @@ import { SerializableState } from '../../kibana_utils/common'; import { EmbeddableInput } from '../common/types'; export interface EmbeddableSetup { + getAttributeService: any; registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index c4fad2917343b..d051793382ab7 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -23,6 +23,8 @@ export interface EmbeddableRegistryDefinition

void; // (undocumented) diff --git a/src/plugins/expressions/.eslintrc.json b/src/plugins/expressions/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/src/plugins/expressions/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 09fb4fae3f201..e8cf497774569 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -24,12 +24,12 @@ export type ExpressionAstNode = | ExpressionAstFunction | ExpressionAstArgument; -export interface ExpressionAstExpression { +export type ExpressionAstExpression = { type: 'expression'; chain: ExpressionAstFunction[]; -} +}; -export interface ExpressionAstFunction { +export type ExpressionAstFunction = { type: 'function'; function: string; arguments: Record; @@ -38,9 +38,9 @@ export interface ExpressionAstFunction { * Debug information added to each function when expression is executed in *debug mode*. */ debug?: ExpressionAstFunctionDebug; -} +}; -export interface ExpressionAstFunctionDebug { +export type ExpressionAstFunctionDebug = { /** * True if function successfully returned output, false if function threw. */ @@ -83,6 +83,6 @@ export interface ExpressionAstFunctionDebug { * timing starts after the arguments have been resolved. */ duration: number | undefined; -} +}; export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index d4c9b0a25d45b..69140453f486d 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { Executor, ExpressionExecOptions } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; @@ -217,7 +218,27 @@ export class Execution< const fn = getByAlias(this.state.get().functions, fnName); if (!fn) { - return createError({ message: `Function ${fnName} could not be found.` }); + return createError({ + name: 'fn not found', + message: i18n.translate('expressions.execution.functionNotFound', { + defaultMessage: `Function {fnName} could not be found.`, + values: { + fnName, + }, + }), + }); + } + + if (fn.disabled) { + return createError({ + name: 'fn is disabled', + message: i18n.translate('expressions.execution.functionDisabled', { + defaultMessage: `Function {fnName} is disabled.`, + values: { + fnName, + }, + }), + }); } let args: Record = {}; diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 20c5b2dd434b5..79bb4c58ab48d 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -62,7 +62,7 @@ export class ExecutionContract< return { type: 'error', error: { - type: e.type, + name: e.name, message: e.message, stack: e.stack, }, diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 81845401d32e4..a658d3457407c 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -21,7 +21,7 @@ import { Executor } from './executor'; import * as expressionTypes from '../expression_types'; import * as expressionFunctions from '../expression_functions'; import { Execution } from '../execution'; -import { parseExpression } from '../ast'; +import { ExpressionAstFunction, parseExpression } from '../ast'; describe('Executor', () => { test('can instantiate', () => { @@ -152,4 +152,47 @@ describe('Executor', () => { }); }); }); + + describe('.inject', () => { + const executor = new Executor(); + + const injectFn = jest.fn().mockImplementation((args, references) => args); + const extractFn = jest.fn().mockReturnValue({ args: {}, references: [] }); + + const fooFn = { + name: 'foo', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + extract: (state: ExpressionAstFunction['arguments']) => { + return extractFn(state); + }, + inject: (state: ExpressionAstFunction['arguments']) => { + return injectFn(state); + }, + fn: jest.fn(), + }; + executor.registerFunction(fooFn); + + test('calls inject function for every expression function in expression', () => { + executor.inject( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + [] + ); + expect(injectFn).toBeCalledTimes(5); + }); + + describe('.extract', () => { + test('calls extract function for every expression function in expression', () => { + executor.extract( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}') + ); + expect(extractFn).toBeCalledTimes(5); + }); + }); + }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 2b5f9f2556d89..28aae8c8f4834 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -19,6 +19,7 @@ /* eslint-disable max-classes-per-file */ +import { cloneDeep, mapValues } from 'lodash'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; @@ -26,9 +27,12 @@ import { Execution, ExecutionParams } from '../execution/execution'; import { IRegistry } from '../types'; import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; -import { ExpressionAstExpression } from '../ast'; +import { ExpressionAstExpression, ExpressionAstFunction } from '../ast'; import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; +import { getByAlias } from '../util'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; export interface ExpressionExecOptions { /** @@ -83,7 +87,8 @@ export class FunctionsRegistry implements IRegistry { } } -export class Executor = Record> { +export class Executor = Record> + implements PersistableState { static createWithDefaults = Record>( state?: ExecutorState ): Executor { @@ -197,6 +202,56 @@ export class Executor = Record void + ) { + for (const link of ast.chain) { + const { function: fnName, arguments: fnArgs } = link; + const fn = getByAlias(this.state.get().functions, fnName); + + if (fn) { + // if any of arguments are expressions we should migrate those first + link.arguments = mapValues(fnArgs, (asts, argName) => { + return asts.map((arg) => { + if (typeof arg === 'object') { + return this.walkAst(arg, action); + } + return arg; + }); + }); + + action(fn, link); + } + } + + return ast; + } + + public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) { + return this.walkAst(cloneDeep(ast), (fn, link) => { + link.arguments = fn.inject(link.arguments, references); + }); + } + + public extract(ast: ExpressionAstExpression) { + const allReferences: SavedObjectReference[] = []; + const newAst = this.walkAst(cloneDeep(ast), (fn, link) => { + const { state, references } = fn.extract(link.arguments); + link.arguments = state; + allReferences.push(...references); + }); + return { state: newAst, references: allReferences }; + } + + public telemetry(ast: ExpressionAstExpression, telemetryData: Record) { + this.walkAst(cloneDeep(ast), (fn, link) => { + telemetryData = fn.telemetry(link.arguments, telemetryData); + }); + + return telemetryData; + } + public fork(): Executor { const initialState = this.state.get(); const fork = new Executor(initialState); diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 71f0d91510136..0b56d3c169ff4 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -17,12 +17,16 @@ * under the License. */ +import { identity } from 'lodash'; import { AnyExpressionFunctionDefinition } from './types'; import { ExpressionFunctionParameter } from './expression_function_parameter'; import { ExpressionValue } from '../expression_types/types'; import { ExecutionContext } from '../execution'; +import { ExpressionAstFunction } from '../ast'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { /** * Name of function */ @@ -60,8 +64,34 @@ export class ExpressionFunction { */ inputTypes: string[] | undefined; + disabled: boolean; + telemetry: ( + state: ExpressionAstFunction['arguments'], + telemetryData: Record + ) => Record; + extract: ( + state: ExpressionAstFunction['arguments'] + ) => { state: ExpressionAstFunction['arguments']; references: SavedObjectReference[] }; + inject: ( + state: ExpressionAstFunction['arguments'], + references: SavedObjectReference[] + ) => ExpressionAstFunction['arguments']; + constructor(functionDefinition: AnyExpressionFunctionDefinition) { - const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition; + const { + name, + type, + aliases, + fn, + help, + args, + inputTypes, + context, + disabled, + telemetry, + inject, + extract, + } = functionDefinition; this.name = name; this.type = type; @@ -70,6 +100,10 @@ export class ExpressionFunction { Promise.resolve(fn(input, params, handlers as ExecutionContext)); this.help = help || ''; this.inputTypes = inputTypes || context?.types; + this.disabled = disabled || false; + this.telemetry = telemetry || ((s, c) => c); + this.inject = inject || identity; + this.extract = extract || ((s) => ({ state: s, references: [] })); for (const [key, arg] of Object.entries(args || {})) { this.args[key] = new ExpressionFunctionParameter(key, arg); diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index d58d872aff722..caaef541aefd5 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -30,6 +30,8 @@ import { ExpressionFunctionVar, ExpressionFunctionTheme, } from './specs'; +import { ExpressionAstFunction } from '../ast'; +import { PersistableStateDefinition } from '../../../kibana_utils/common'; /** * `ExpressionFunctionDefinition` is the interface plugins have to implement to @@ -41,12 +43,17 @@ export interface ExpressionFunctionDefinition< Arguments extends Record, Output, Context extends ExecutionContext = ExecutionContext -> { +> extends PersistableStateDefinition { /** * The name of the function, as will be used in expression. */ name: Name; + /** + * if set to true function will be disabled (but its migrate function will still be available) + */ + disabled?: boolean; + /** * Name of type of value this function outputs. */ diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index b760e7b32a7d2..0ea3d72e75609 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PersistedState } from 'src/plugins/visualizations/public'; + export interface ExpressionRenderDefinition { /** * Technical name of the renderer, used as ID to identify renderer in @@ -68,4 +70,5 @@ export interface IInterpreterRenderHandlers { reload: () => void; update: (params: any) => void; event: (event: any) => void; + uiState?: PersistedState; } diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index ebaedcbba0d23..7607945d8a664 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -20,20 +20,16 @@ import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { ExpressionValueRender } from './render'; import { getType } from '../get_type'; +import { SerializableState } from '../../../../kibana_utils/common'; +import { ErrorLike } from '../../util'; const name = 'error'; export type ExpressionValueError = ExpressionValueBoxed< 'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; } >; diff --git a/src/plugins/expressions/common/expression_types/specs/range.ts b/src/plugins/expressions/common/expression_types/specs/range.ts index 3d7170cf715d7..53fd4894fd2be 100644 --- a/src/plugins/expressions/common/expression_types/specs/range.ts +++ b/src/plugins/expressions/common/expression_types/specs/range.ts @@ -26,6 +26,7 @@ export interface Range { type: typeof name; from: number; to: number; + label?: string; } export const range: ExpressionTypeDefinition = { @@ -41,7 +42,7 @@ export const range: ExpressionTypeDefinition = { }, to: { render: (value: Range): ExpressionValueRender<{ text: string }> => { - const text = `from ${value.from} to ${value.to}`; + const text = value?.label || `from ${value.from} to ${value.to}`; return { type: 'render', as: 'text', diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index b5c98fada07c4..4a87fd9e7f331 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -23,6 +23,8 @@ import { ExpressionAstExpression } from '../ast'; import { ExecutionContract } from '../execution/execution_contract'; import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -154,7 +156,7 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService { +export class ExpressionsService implements PersistableState { public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -256,6 +258,36 @@ export class ExpressionsService { return fork; }; + /** + * Extracts telemetry from expression AST + * @param state expression AST to extract references from + */ + public readonly telemetry = ( + state: ExpressionAstExpression, + telemetryData: Record = {} + ) => { + return this.executor.telemetry(state, telemetryData); + }; + + /** + * Extracts saved object references from expression AST + * @param state expression AST to extract references from + * @returns new expression AST with references removed and array of references + */ + public readonly extract = (state: ExpressionAstExpression) => { + return this.executor.extract(state); + }; + + /** + * Injects saved object references into expression AST + * @param state expression AST to update + * @param references array of saved object references + * @returns new expression AST with references injected + */ + public readonly inject = (state: ExpressionAstExpression, references: SavedObjectReference[]) => { + return this.executor.inject(state, references); + }; + /** * Returns Kibana Platform *setup* life-cycle contract. Useful to return the * same contract on server-side and browser-side. diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 9bdab74efd6f9..293afd46d4de5 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -19,9 +19,17 @@ import { ExpressionValueError } from '../../common'; -type ErrorLike = Partial>; +export type SerializedError = { + name: string; + message: string; + stack?: string; +}; -export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({ +export type ErrorLike = SerializedError & { + original?: SerializedError; +}; + +export const createError = (err: string | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -32,6 +40,11 @@ export const createError = (err: string | Error | ErrorLike): ExpressionValueErr : undefined, message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', - original: err instanceof Error ? err : undefined, + original: + err instanceof Error + ? err + : typeof err === 'object' && 'original' in err && err.original instanceof Error + ? err.original + : undefined, }, }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 5c0fd8ab1a572..c7b6190b96ed7 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -11,6 +11,7 @@ import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; +import { PersistedState } from 'src/plugins/visualizations/public'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import React from 'react'; @@ -188,10 +189,11 @@ export interface ExecutionState extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } +// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> { +export class Executor = Record> implements PersistableState { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -202,6 +204,11 @@ export class Executor = Record): void; // (undocumented) + extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; + // (undocumented) fork(): Executor; // @deprecated (undocumented) readonly functions: FunctionsRegistry; @@ -213,6 +220,10 @@ export class Executor = Record; + // Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; // (undocumented) registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -220,9 +231,11 @@ export class Executor = Record = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; // (undocumented) readonly state: ExecutorContainer; + // (undocumented) + telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; // @deprecated (undocumented) readonly types: TypesRegistry; -} + } // Warning: (ae-forgotten-export) The symbol "ExecutorPureTransitions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ExecutorPureSelectors" needs to be exported by the entry point index.d.ts @@ -251,12 +264,10 @@ export type ExpressionAstArgument = string | boolean | number | ExpressionAstExp // Warning: (ae-missing-release-tag) "ExpressionAstExpression" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstExpression { - // (undocumented) - chain: ExpressionAstFunction[]; - // (undocumented) +export type ExpressionAstExpression = { type: 'expression'; -} + chain: ExpressionAstFunction[]; +}; // Warning: (ae-missing-release-tag) "ExpressionAstExpressionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -272,16 +283,12 @@ export interface ExpressionAstExpressionBuilder { // Warning: (ae-missing-release-tag) "ExpressionAstFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstFunction { - // (undocumented) +export type ExpressionAstFunction = { + type: 'function'; + function: string; arguments: Record; - // Warning: (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts debug?: ExpressionAstFunctionDebug; - // (undocumented) - function: string; - // (undocumented) - type: 'function'; -} +}; // Warning: (ae-missing-release-tag) "ExpressionAstFunctionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -319,23 +326,35 @@ export interface ExpressionExecutor { // Warning: (ae-missing-release-tag) "ExpressionFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { constructor(functionDefinition: AnyExpressionFunctionDefinition); // (undocumented) accepts: (type: string) => boolean; aliases: string[]; args: Record; + // (undocumented) + disabled: boolean; + // (undocumented) + extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; fn: (input: ExpressionValue, params: Record, handlers: object) => ExpressionValue; help: string; + // (undocumented) + inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; name: string; + // (undocumented) + telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; type: string; } +// Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> { +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition { aliases?: string[]; args: { [key in keyof Arguments]: ArgumentType; @@ -344,6 +363,7 @@ export interface ExpressionFunctionDefinition>; @@ -491,6 +511,8 @@ export class ExpressionRendererRegistry implements IRegistry // // @public (undocumented) export interface ExpressionRenderError extends Error { + // (undocumented) + original?: Error; // (undocumented) type?: string; } @@ -539,13 +561,17 @@ export { ExpressionsPublicPlugin as Plugin } // Warning: (ae-missing-release-tag) "ExpressionsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export class ExpressionsService { +export class ExpressionsService implements PersistableState { // Warning: (ae-forgotten-export) The symbol "ExpressionServiceParams" needs to be exported by the entry point index.d.ts constructor({ executor, renderers, }?: ExpressionServiceParams); // (undocumented) readonly execute: ExpressionsServiceStart['execute']; // (undocumented) readonly executor: Executor; + readonly extract: (state: ExpressionAstExpression) => { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; // (undocumented) readonly fork: () => ExpressionsService; // (undocumented) @@ -557,6 +583,7 @@ export class ExpressionsService { // (undocumented) readonly getType: ExpressionsServiceStart['getType']; readonly getTypes: () => ReturnType; + readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; readonly registerFunction: (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void; // (undocumented) readonly registerRenderer: (definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) => void; @@ -570,6 +597,7 @@ export class ExpressionsService { start(): ExpressionsServiceStart; // (undocumented) stop(): void; + readonly telemetry: (state: ExpressionAstExpression, telemetryData?: Record) => Record; } // Warning: (ae-missing-release-tag) "ExpressionsServiceSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -704,14 +732,8 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -883,6 +905,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) reload: () => void; // (undocumented) + uiState?: PersistedState; + // (undocumented) update: (params: any) => void; } @@ -1045,6 +1069,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // @@ -1073,7 +1099,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; reload$?: Observable; // (undocumented) - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; } // Warning: (ae-missing-release-tag) "ReactExpressionRendererType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1159,6 +1185,12 @@ export type TypeToString = KnownTypeToString | UnmappedTypeStrings; export type UnmappedTypeStrings = 'date' | 'filter'; +// Warnings were encountered during analysis: +// +// src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 12476c70044b5..99d170c96666d 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -35,7 +35,10 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { className?: string; dataAttrs?: string[]; expression: string | ExpressionAstExpression; - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: ( + message?: string | null, + error?: ExpressionRenderError | null + ) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; onEvent?: (event: ExpressionRendererEvent) => void; /** @@ -186,7 +189,10 @@ export const ReactExpressionRenderer = ({

{state.isEmpty && } {state.isLoading && } - {!state.isLoading && state.error && renderError && renderError(state.error.message)} + {!state.isLoading && + state.error && + renderError && + renderError(state.error.message, state.error)}
extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } +// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> { +export class Executor = Record> implements PersistableState { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -184,6 +186,11 @@ export class Executor = Record): void; // (undocumented) + extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; + // (undocumented) fork(): Executor; // @deprecated (undocumented) readonly functions: FunctionsRegistry; @@ -195,6 +202,10 @@ export class Executor = Record; + // Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; // (undocumented) registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -202,9 +213,11 @@ export class Executor = Record = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; // (undocumented) readonly state: ExecutorContainer; + // (undocumented) + telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; // @deprecated (undocumented) readonly types: TypesRegistry; -} + } // Warning: (ae-forgotten-export) The symbol "ExecutorPureTransitions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ExecutorPureSelectors" needs to be exported by the entry point index.d.ts @@ -233,12 +246,10 @@ export type ExpressionAstArgument = string | boolean | number | ExpressionAstExp // Warning: (ae-missing-release-tag) "ExpressionAstExpression" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstExpression { - // (undocumented) - chain: ExpressionAstFunction[]; - // (undocumented) +export type ExpressionAstExpression = { type: 'expression'; -} + chain: ExpressionAstFunction[]; +}; // Warning: (ae-missing-release-tag) "ExpressionAstExpressionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -254,16 +265,12 @@ export interface ExpressionAstExpressionBuilder { // Warning: (ae-missing-release-tag) "ExpressionAstFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstFunction { - // (undocumented) +export type ExpressionAstFunction = { + type: 'function'; + function: string; arguments: Record; - // Warning: (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts debug?: ExpressionAstFunctionDebug; - // (undocumented) - function: string; - // (undocumented) - type: 'function'; -} +}; // Warning: (ae-missing-release-tag) "ExpressionAstFunctionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -291,23 +298,35 @@ export type ExpressionAstNode = ExpressionAstExpression | ExpressionAstFunction // Warning: (ae-missing-release-tag) "ExpressionFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { constructor(functionDefinition: AnyExpressionFunctionDefinition); // (undocumented) accepts: (type: string) => boolean; aliases: string[]; args: Record; + // (undocumented) + disabled: boolean; + // (undocumented) + extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; fn: (input: ExpressionValue, params: Record, handlers: object) => ExpressionValue; help: string; + // (undocumented) + inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; name: string; + // (undocumented) + telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; type: string; } +// Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> { +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition { aliases?: string[]; args: { [key in keyof Arguments]: ArgumentType; @@ -316,6 +335,7 @@ export interface ExpressionFunctionDefinition>; @@ -564,14 +584,8 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -717,6 +731,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) reload: () => void; // (undocumented) + uiState?: PersistedState; + // (undocumented) update: (params: any) => void; } @@ -878,6 +894,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // @@ -963,6 +981,12 @@ export type TypeToString = KnownTypeToString | UnmappedTypeStrings; export type UnmappedTypeStrings = 'date' | 'filter'; +// Warnings were encountered during analysis: +// +// src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts index 736e79015af9f..54fed3db1de4d 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts @@ -17,39 +17,39 @@ * under the License. */ -import sinon from 'sinon'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { fetchProvider } from './collector_fetch'; -describe('Sample Data Fetch', () => { - let callClusterMock: sinon.SinonStub; +const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; - beforeEach(() => { - callClusterMock = sinon.stub(); - }); +describe('Sample Data Fetch', () => { + let collectorFetchContext: CollectorFetchContext; test('uninitialized .kibana', async () => { const fetch = fetchProvider('index'); - const telemetry = await fetch(callClusterMock); + collectorFetchContext = getMockFetchClients(); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(`undefined`); }); test('installed data set', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -67,27 +67,23 @@ Object { test('multiple installed data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -106,17 +102,13 @@ Object { test('installed data set, missing counts', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -132,34 +124,30 @@ Object { test('installed and uninstalled data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test0', - _source: { - updated_at: '2019-03-13T22:29:32Z', - 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, - }, - }, - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test0', + _source: { + updated_at: '2019-03-13T22:29:32Z', + 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, + }, + }, + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts index d43458cfc64db..7df9b14d2efb1 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts @@ -19,6 +19,7 @@ import { get } from 'lodash'; import moment from 'moment'; +import { CollectorFetchContext } from '../../../../../usage_collection/server'; interface SearchHit { _id: string; @@ -41,7 +42,7 @@ export interface TelemetryResponse { } export function fetchProvider(index: string) { - return async (callCluster: any) => { + return async ({ callCluster }: CollectorFetchContext) => { const response = await callCluster('search', { index, body: { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx index 39c0add40e9ad..076cda484d808 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx @@ -26,31 +26,31 @@ import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { CreateEditField } from './create_edit_field'; -export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName: string }>; +export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName?: string }>; const CreateEditFieldCont: React.FC = ({ ...props }) => { const { setBreadcrumbs, data } = useKibana().services; const [indexPattern, setIndexPattern] = useState(); + const fieldName = + props.match.params.fieldName && decodeURIComponent(props.match.params.fieldName); useEffect(() => { data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => { setIndexPattern(ip); if (ip) { setBreadcrumbs( - props.match.params.fieldName - ? getEditFieldBreadcrumbs(ip, props.match.params.fieldName) - : getCreateFieldBreadcrumbs(ip) + fieldName ? getEditFieldBreadcrumbs(ip, fieldName) : getCreateFieldBreadcrumbs(ip) ); } }); - }, [props.match.params.id, props.match.params.fieldName, setBreadcrumbs, data.indexPatterns]); + }, [props.match.params.id, fieldName, setBreadcrumbs, data.indexPatterns]); if (indexPattern) { return ( ); } else { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts new file mode 100644 index 0000000000000..0e3ee27476fcc --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getPath } from './utils'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; + +test('getPath() should encode "fieldName"', () => { + expect( + getPath( + ({ name: 'Memory: Allocated Bytes/sec' } as unknown) as IndexPatternField, + ({ id: 'id' } as unknown) as IndexPattern + ) + ).toMatchInlineSnapshot(`"/patterns/id/field/Memory%3A%20Allocated%20Bytes%2Fsec"`); +}); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index 91c5cc1afdb49..a94ed60b7aed5 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -117,7 +117,7 @@ export function getTabs( } export function getPath(field: IndexPatternField, indexPattern: IndexPattern) { - return `/patterns/${indexPattern?.id}/field/${field.name}`; + return `/patterns/${indexPattern?.id}/field/${encodeURIComponent(field.name)}`; } const allTypesDropDown = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap index 69b192a81d097..38f630358d064 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap @@ -4,6 +4,7 @@ exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; exports[`LabelTemplateFlyout should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index f862d0ebe8477..13be0353e1640 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -1,544 +1,431 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UrlFormatEditor should render label template help 1`] = ` - - - - - } - labelType="label" +exports[`UrlFormatEditor should render normally 1`] = ` +
+
- - - + - + Type + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render normally 1`] = ` - - - - - } - labelType="label" +
+ +
+ + +
+
+
+
+
- - - + - + Open in a new tab + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" + + + + Off + + +
+
+
+
- - - - -`; - -exports[`UrlFormatEditor should render url template help 1`] = ` - - - - - } - labelType="label" - > - - - + - + URL template + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render width and height fields if image 1`] = ` - - - - - } - labelType="label" - > - - - + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" + +
+
+
+
- - - + - - } - labelType="label" - > - - - - } - labelType="label" + + Label template + + +
+
+
+
+ +
+
+
+ +
+
+
+
- - - - +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Input + +
+
+
+ + Output + +
+
+
+ Input +
+
+ john +
+
+
+ Output +
+
+
+ converted url for john +
+
+
+
+ Input +
+
+ /some/pathname/asset.png +
+
+
+ Output +
+
+
+ converted url for /some/pathname/asset.png +
+
+
+
+ Input +
+
+ 1234 +
+
+
+ Output +
+
+
+ converted url for 1234 +
+
+
+
+
+
+
+
`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap index 14e5012e9a554..83e815dd72661 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap @@ -4,6 +4,7 @@ exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; exports[`UrlTemplateFlyout should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx index d04ee58f26b0a..4dd3fb8f1b695 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx @@ -63,7 +63,7 @@ const items: LabelTemplateExampleItem[] = [ export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { return isVisible ? ( - +

diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx index a1a1655949432..eb5cac111928f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx @@ -18,13 +18,22 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; import { FieldFormat } from 'src/plugins/data/public'; - +import { IntlProvider } from 'react-intl'; import { UrlFormatEditor } from './url'; +import { coreMock } from '../../../../../../../../../core/public/mocks'; +import { createKibanaReactContext } from '../../../../../../../../kibana_react/public'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@elastic/eui/lib/services/accessibility', () => { + return { + htmlIdGenerator: () => () => `generated-id`, + }; +}); const fieldType = 'string'; -const format = { +const format = ({ getConverterFor: jest .fn() .mockImplementation(() => (input: string) => `converted url for ${input}`), @@ -35,78 +44,115 @@ const format = { { kind: 'audio', text: 'Audio' }, ], }, -}; +} as unknown) as FieldFormat; const formatParams = { openLinkInCurrentTab: true, urlTemplate: '', labelTemplate: '', width: '', height: '', + type: 'a', }; + const onChange = jest.fn(); const onError = jest.fn(); +const renderWithContext = (Element: React.ReactElement) => + render( + + {Element} + + ); + +const MY_BASE_PATH = 'my-base-path'; +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('UrlFormatEditor', () => { it('should have a formatId', () => { expect(UrlFormatEditor.formatId).toEqual('url'); }); it('should render normally', async () => { - const component = shallow( + const { container } = renderWithContext( ); - - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should render url template help', async () => { - const component = shallow( + const { getByText, getByTestId } = renderWithContext( ); - (component.instance() as UrlFormatEditor).showUrlTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); + getByText('URL template help'); + userEvent.click(getByText('URL template help')); + expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible(); }); it('should render label template help', async () => { - const component = shallow( + const { getByText, getByTestId } = renderWithContext( ); - (component.instance() as UrlFormatEditor).showLabelTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); + getByText('Label template help'); + userEvent.click(getByText('Label template help')); + expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible(); }); it('should render width and height fields if image', async () => { - const component = shallow( + const { getByLabelText } = renderWithContext( ); - expect(component).toMatchSnapshot(); + expect(getByLabelText('Width')).toBeInTheDocument(); + expect(getByLabelText('Height')).toBeInTheDocument(); + }); + + it('should append base path to preview images', async () => { + let sampleImageUrlTemplate = ''; + const { getByLabelText } = renderWithContext( + { + sampleImageUrlTemplate = urlTemplate; + }} + onError={onError} + /> + ); + + // TODO: sample image url emitted only during change event + // So can't just path `type: img` and check rendered value + userEvent.selectOptions(getByLabelText('Type'), 'img'); + expect(sampleImageUrlTemplate).toContain(MY_BASE_PATH); + expect(sampleImageUrlTemplate).toMatchInlineSnapshot( + `"my-base-path/plugins/indexPatternManagement/assets/icons/{{value}}.png"` + ); }); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx index 30acf09526f85..95b5fc3955280 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx @@ -36,6 +36,8 @@ import { FormatEditorSamples } from '../../samples'; import { LabelTemplateFlyout } from './label_template_flyout'; import { UrlTemplateFlyout } from './url_template_flyout'; +import type { IndexPatternManagmentContextValue } from '../../../../../../types'; +import { context as contextType } from '../../../../../../../../kibana_react/public'; interface OnChangeParam { type: string; @@ -66,14 +68,21 @@ export class UrlFormatEditor extends DefaultFormatEditor< UrlFormatEditorFormatParams, UrlFormatEditorFormatState > { + static contextType = contextType; static formatId = 'url'; - iconPattern: string; + // TODO: @kbn/optimizer can't compile this + // declare context: IndexPatternManagmentContextValue; + context: IndexPatternManagmentContextValue | undefined; + private get sampleIconPath() { + const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; + return this.context?.services.http + ? this.context.services.http.basePath.prepend(sampleIconPath) + : sampleIconPath; + } constructor(props: FormatEditorProps) { super(props); - this.iconPattern = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; - this.state = { ...this.state, sampleInputsByType: { @@ -104,9 +113,9 @@ export class UrlFormatEditor extends DefaultFormatEditor< params.width = width; params.height = height; if (!urlTemplate) { - params.urlTemplate = this.iconPattern; + params.urlTemplate = this.sampleIconPath; } - } else if (newType !== 'img' && urlTemplate === this.iconPattern) { + } else if (newType !== 'img' && urlTemplate === this.sampleIconPath) { params.urlTemplate = undefined; } this.onChange(params); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx index c1b144b0d9eac..b1c66874d69cf 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const UrlTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { return isVisible ? ( - +

diff --git a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx index 6da5be450beb0..f424b25526b16 100644 --- a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx +++ b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx @@ -49,8 +49,6 @@ function IndexPatternSelectFormRowUi(props: IndexPatternSelectFormRowUiProps) { indexPatternId={indexPatternId} onChange={onChange} data-test-subj={selectId} - // TODO: supply actual savedObjectsClient here - savedObjectsClient={{} as any} /> ); diff --git a/src/plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts index da2dc7bab7cf7..303b340e5905b 100644 --- a/src/plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -83,7 +83,7 @@ export abstract class Control { format = (value: any) => { const indexPattern = this.filterManager.getIndexPattern(); const field = this.filterManager.getField(); - if (field) { + if (field && indexPattern) { return indexPattern.getFormatterForField(field).convert(value); } diff --git a/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index e0eeddf93f67e..141f879f248a9 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -21,7 +21,11 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; import { coreMock } from '../../../../../core/public/mocks'; -import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; +import { + Filter, + FilterManager as QueryFilterManager, + IndexPatternsContract, +} from '../../../../data/public'; const setupMock = coreMock.createSetup(); @@ -39,7 +43,6 @@ describe('FilterManager', function () { const controlId = 'control1'; describe('findFilters', function () { - const indexPatternMock = {} as IndexPattern; let kbnFilters: Filter[]; const queryFilterMock = new QueryFilterManager(setupMock.uiSettings); queryFilterMock.getAppFilters = () => kbnFilters; @@ -48,7 +51,13 @@ describe('FilterManager', function () { let filterManager: FilterManagerTest; beforeEach(() => { kbnFilters = []; - filterManager = new FilterManagerTest(controlId, 'field1', indexPatternMock, queryFilterMock); + filterManager = new FilterManagerTest( + controlId, + 'field1', + '1', + {} as IndexPatternsContract, + queryFilterMock + ); }); test('should not find filters that are not controlled by any visualization', function () { diff --git a/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index ece3f7a88ba37..de948ccda8663 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,14 +19,22 @@ import _ from 'lodash'; -import { FilterManager as QueryFilterManager, IndexPattern, Filter } from '../../../../data/public'; +import { + FilterManager as QueryFilterManager, + IndexPattern, + Filter, + IndexPatternsContract, +} from '../../../../data/public'; export abstract class FilterManager { + protected indexPattern: IndexPattern | undefined; + constructor( public controlId: string, public fieldName: string, - public indexPattern: IndexPattern, - public queryFilter: QueryFilterManager + private indexPatternId: string, + private indexPatternsService: IndexPatternsContract, + protected queryFilter: QueryFilterManager ) {} /** @@ -41,12 +49,22 @@ export abstract class FilterManager { abstract getValueFromFilterBar(): any; - getIndexPattern(): IndexPattern { + async init() { + try { + if (!this.indexPattern) { + this.indexPattern = await this.indexPatternsService.get(this.indexPatternId); + } + } catch (e) { + // invalid index pattern id + } + } + + getIndexPattern(): IndexPattern | undefined { return this.indexPattern; } getField() { - return this.indexPattern.fields.getByName(this.fieldName); + return this.indexPattern?.fields.getByName(this.fieldName); } findFilters(): Filter[] { @@ -54,7 +72,7 @@ export abstract class FilterManager { this.queryFilter.getAppFilters(), this.queryFilter.getGlobalFilters(), ]); - return kbnFilters.filter((kbnFilter) => { + return kbnFilters.filter((kbnFilter: Filter) => { return _.get(kbnFilter, 'meta.controlledBy') === this.controlId; }); } diff --git a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index ec1f9d42aa67b..066d87230f909 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -19,7 +19,12 @@ import expect from '@kbn/expect'; -import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; +import { + Filter, + IndexPattern, + FilterManager as QueryFilterManager, + IndexPatternsContract, +} from '../../../../data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function () { @@ -42,15 +47,20 @@ describe('PhraseFilterManager', function () { }, }, } as IndexPattern; + const indexPatternsServiceMock = ({ + get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), + } as unknown) as jest.Mocked; const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; let filterManager: PhraseFilterManager; - beforeEach(() => { + beforeEach(async () => { filterManager = new PhraseFilterManager( controlId, 'field1', - indexPatternMock, + '1', + indexPatternsServiceMock, queryFilterMock ); + await filterManager.init(); }); test('should create match phrase filter from single value', function () { @@ -89,10 +99,11 @@ describe('PhraseFilterManager', function () { constructor( id: string, fieldName: string, - indexPattern: IndexPattern, + indexPatternId: string, + indexPatternsService: IndexPatternsContract, queryFilter: QueryFilterManager ) { - super(id, fieldName, indexPattern, queryFilter); + super(id, fieldName, indexPatternId, indexPatternsService, queryFilter); this.mockFilters = []; } @@ -105,14 +116,15 @@ describe('PhraseFilterManager', function () { } } - const indexPatternMock: IndexPattern = {} as IndexPattern; + const indexPatternsServiceMock = {} as IndexPatternsContract; const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; let filterManager: MockFindFiltersPhraseFilterManager; beforeEach(() => { filterManager = new MockFindFiltersPhraseFilterManager( controlId, 'field1', - indexPatternMock, + '1', + indexPatternsServiceMock, queryFilterMock ); }); diff --git a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 03ed6c5520dec..f3a2b4085b923 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -23,7 +23,7 @@ import { FilterManager } from './filter_manager'; import { PhraseFilter, esFilters, - IndexPattern, + IndexPatternsContract, FilterManager as QueryFilterManager, } from '../../../../data/public'; @@ -31,24 +31,26 @@ export class PhraseFilterManager extends FilterManager { constructor( controlId: string, fieldName: string, - indexPattern: IndexPattern, + indexPatternId: string, + indexPatternsService: IndexPatternsContract, queryFilter: QueryFilterManager ) { - super(controlId, fieldName, indexPattern, queryFilter); + super(controlId, fieldName, indexPatternId, indexPatternsService, queryFilter); } createFilter(phrases: any): PhraseFilter { + const indexPattern = this.getIndexPattern()!; let newFilter: PhraseFilter; - const value = this.indexPattern.fields.getByName(this.fieldName); + const value = indexPattern.fields.getByName(this.fieldName); if (!value) { throw new Error(`Unable to find field with name: ${this.fieldName} on indexPattern`); } if (phrases.length === 1) { - newFilter = esFilters.buildPhraseFilter(value, phrases[0], this.indexPattern); + newFilter = esFilters.buildPhraseFilter(value, phrases[0], indexPattern); } else { - newFilter = esFilters.buildPhrasesFilter(value, phrases, this.indexPattern); + newFilter = esFilters.buildPhrasesFilter(value, phrases, indexPattern); } newFilter.meta.key = this.fieldName; diff --git a/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index 8556312d10f4a..2a66567006049 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -25,6 +25,7 @@ import { RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, + IndexPatternsContract, } from '../../../../data/public'; describe('RangeFilterManager', function () { @@ -46,15 +47,20 @@ describe('RangeFilterManager', function () { }, }, } as IndexPattern; + const indexPatternsServiceMock = ({ + get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), + } as unknown) as jest.Mocked; const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; let filterManager: RangeFilterManager; - beforeEach(() => { + beforeEach(async () => { filterManager = new RangeFilterManager( controlId, 'field1', - indexPatternMock, + '1', + indexPatternsServiceMock, queryFilterMock ); + await filterManager.init(); }); test('should create range filter from slider value', function () { @@ -75,10 +81,11 @@ describe('RangeFilterManager', function () { constructor( id: string, fieldName: string, - indexPattern: IndexPattern, + indexPatternId: string, + indexPatternsService: IndexPatternsContract, queryFilter: QueryFilterManager ) { - super(id, fieldName, indexPattern, queryFilter); + super(id, fieldName, indexPatternId, indexPatternsService, queryFilter); this.mockFilters = []; } @@ -91,14 +98,15 @@ describe('RangeFilterManager', function () { } } - const indexPatternMock: IndexPattern = {} as IndexPattern; + const indexPatternsServiceMock = {} as IndexPatternsContract; const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; let filterManager: MockFindFiltersRangeFilterManager; beforeEach(() => { filterManager = new MockFindFiltersRangeFilterManager( controlId, 'field1', - indexPatternMock, + '1', + indexPatternsServiceMock, queryFilterMock ); }); diff --git a/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 1a884cf267c41..39e4bfa19e5dc 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -61,11 +61,12 @@ export class RangeFilterManager extends FilterManager { * @return {object} range filter */ createFilter(value: SliderValue): RangeFilter { + const indexPattern = this.getIndexPattern()!; const newFilter = esFilters.buildRangeFilter( // TODO: Fix type to be required - this.indexPattern.fields.getByName(this.fieldName) as IFieldType, + indexPattern.fields.getByName(this.fieldName) as IFieldType, toRange(value), - this.indexPattern + indexPattern ); newFilter.meta.key = this.fieldName; newFilter.meta.controlledBy = this.controlId; diff --git a/src/plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts index 325eb471d510b..74ee09117887e 100644 --- a/src/plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -142,6 +142,17 @@ export class ListControl extends Control { timeout: `${settings.autocompleteTimeout}ms`, terminate_after: Number(settings.autocompleteTerminateAfter), }; + + // dynamic options are only allowed on String fields but the setting defaults to true so it could + // be enabled for non-string fields (since UI input is hidden for non-string fields). + // If field is not string, then disable dynamic options. + const field = indexPattern?.fields + .getAll() + .find(({ name }) => name === this.controlParams.fieldName); + if (field && field.type !== 'string') { + this.options.dynamicOptions = false; + } + const aggs = termsAgg({ field: indexPattern.fields.getByName(fieldName), size: this.options.dynamicOptions ? null : _.get(this.options, 'size', 5), @@ -213,27 +224,20 @@ export async function listControlFactory( deps: InputControlVisDependencies ) { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); - const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern); - - // dynamic options are only allowed on String fields but the setting defaults to true so it could - // be enabled for non-string fields (since UI input is hidden for non-string fields). - // If field is not string, then disable dynamic options. - const field = indexPattern.fields.getAll().find(({ name }) => name === controlParams.fieldName); - if (field && field.type !== 'string') { - controlParams.options.dynamicOptions = false; - } const listControl = new ListControl( controlParams, new PhraseFilterManager( controlParams.id, controlParams.fieldName, - indexPattern, + controlParams.indexPattern, + dataPluginStart.indexPatterns, deps.data.query.filterManager ), useTimeFilter, dataPluginStart.search.searchSource, deps ); + await listControl.filterManager.init(); return listControl; } diff --git a/src/plugins/input_control_vis/public/control/range_control_factory.ts b/src/plugins/input_control_vis/public/control/range_control_factory.ts index eac79ca5fcca8..9cb1a0e25a8db 100644 --- a/src/plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/range_control_factory.ts @@ -134,18 +134,20 @@ export async function rangeControlFactory( deps: InputControlVisDependencies ): Promise { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); - const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern); - return new RangeControl( + const rangeControl = new RangeControl( controlParams, new RangeFilterManager( controlParams.id, controlParams.fieldName, - indexPattern, + controlParams.indexPattern, + dataPluginStart.indexPatterns, deps.data.query.filterManager ), useTimeFilter, dataPluginStart.search.searchSource, deps ); + await rangeControl.filterManager.init(); + return rangeControl; } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 23a77c2d4c288..c1457c64080a6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -17,16 +17,13 @@ * under the License. */ -import { - savedObjectsRepositoryMock, - loggingSystemMock, - elasticsearchServiceMock, -} from '../../../../../core/server/mocks'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL, @@ -53,8 +50,7 @@ describe('telemetry_application_usage', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) @@ -67,7 +63,7 @@ describe('telemetry_application_usage', () => { test('if no savedObjectClient initialised, return undefined', async () => { expect(collector.isReady()).toBe(false); - expect(await collector.fetch(callCluster, esClient)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); jest.runTimersToTime(ROLL_INDICES_START); }); @@ -85,7 +81,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -142,7 +138,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: total + 1 + 10, clicks_7_days: total + 1, @@ -202,7 +198,7 @@ describe('telemetry_application_usage', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: 1, clicks_7_days: 0, diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts index b712e9ebbce48..e8efa9997c459 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts @@ -21,7 +21,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerCoreUsageCollector } from '.'; import { coreUsageDataServiceMock } from '../../../../../core/server/mocks'; import { CoreUsageData } from 'src/core/server/'; @@ -35,7 +35,7 @@ describe('telemetry_core', () => { return createUsageCollectionSetupMock().makeUsageCollector(config); }); - const callCluster = jest.fn().mockImplementation(() => ({})); + const collectorFetchContext = createCollectorFetchContextMock(); const coreUsageDataStart = coreUsageDataServiceMock.createStartContract(); const getCoreUsageDataReturnValue = (Symbol('core telemetry') as any) as CoreUsageData; coreUsageDataStart.getCoreUsageData.mockResolvedValue(getCoreUsageDataReturnValue); @@ -48,6 +48,6 @@ describe('telemetry_core', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toEqual(getCoreUsageDataReturnValue); + expect(await collector.fetch(collectorFetchContext)).toEqual(getCoreUsageDataReturnValue); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts index 465b21e3578ba..03184d7385861 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts @@ -20,10 +20,12 @@ import { CspConfig, ICspConfig } from '../../../../../core/server'; import { createCspCollector } from './csp_collector'; import { httpServiceMock } from '../../../../../core/server/mocks'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; describe('csp collector', () => { let httpMock: ReturnType; - const mockCallCluster = null as any; + // changed for consistency with expected implementation + const mockedFetchContext = createCollectorFetchContextMock(); function updateCsp(config: Partial) { httpMock.csp = new CspConfig(config); @@ -36,28 +38,28 @@ describe('csp collector', () => { test('fetches whether strict mode is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(true); updateCsp({ strict: false }); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(false); }); test('fetches whether the legacy browser warning is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(true); updateCsp({ warnLegacyBrowsers: false }); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(false); }); test('fetches whether the csp rules have been changed or not', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(false); updateCsp({ rules: ['not', 'default'] }); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(true); }); test('does not include raw csp rules under any property names', async () => { @@ -69,7 +71,7 @@ describe('csp collector', () => { // // We use a snapshot here to ensure csp.rules isn't finding its way into the // payload under some new and unexpected variable name (e.g. cspRules). - expect(await collector.fetch(mockCallCluster)).toMatchInlineSnapshot(` + expect(await collector.fetch(mockedFetchContext)).toMatchInlineSnapshot(` Object { "rulesChangedFromDefault": false, "strict": true, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts index 2bfe59d7dd4fc..88ccb2016d420 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts @@ -22,7 +22,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerKibanaUsageCollector } from './'; describe('telemetry_kibana', () => { @@ -35,7 +35,12 @@ describe('telemetry_kibana', () => { }); const legacyConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; - const callCluster = jest.fn().mockImplementation(() => ({})); + + const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; + }; beforeAll(() => registerKibanaUsageCollector(usageCollectionMock, legacyConfig$)); afterAll(() => jest.clearAllTimers()); @@ -46,7 +51,7 @@ describe('telemetry_kibana', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(getMockFetchClients())).toStrictEqual({ index: '.kibana-tests', dashboard: { total: 0 }, visualization: { total: 0 }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts index 5b56e1a9b596f..d292b2d5ace0e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts @@ -44,7 +44,7 @@ export function getKibanaUsageCollector( graph_workspace: { total: { type: 'long' } }, timelion_sheet: { total: { type: 'long' } }, }, - async fetch(callCluster) { + async fetch({ callCluster }) { const { kibana: { index }, } = await legacyConfig$.pipe(take(1)).toPromise(); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts index d4b635448d0a3..e671f739ee083 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts @@ -21,6 +21,7 @@ import { uiSettingsServiceMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerManagementUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_application_usage_collector', () => { const uiSettingsClient = uiSettingsServiceMock.createClient(); const getUiSettingsClient = jest.fn(() => uiSettingsClient); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => { registerManagementUsageCollector(usageCollectionMock, getUiSettingsClient); @@ -59,11 +60,11 @@ describe('telemetry_application_usage_collector', () => { uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({ 'my-key': { userValue: 'my-value' }, })); - await expect(collector.fetch(callCluster)).resolves.toMatchSnapshot(); + await expect(collector.fetch(mockedFetchContext)).resolves.toMatchSnapshot(); }); test('fetch() should not fail if invoked when not ready', async () => { getUiSettingsClient.mockImplementationOnce(() => undefined as any); - await expect(collector.fetch(callCluster)).resolves.toBe(undefined); + await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts index a527d4d03c6fc..61990730812cc 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts @@ -21,6 +21,7 @@ import { Subject } from 'rxjs'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerOpsStatsCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ops_stats', () => { }); const metrics$ = new Subject(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); const metric: OpsMetrics = { collected_at: new Date('2020-01-01 01:00:00'), @@ -92,7 +93,7 @@ describe('telemetry_ops_stats', () => { test('should return something when there is a metric', async () => { metrics$.next(metric); expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster)).toMatchSnapshot({ + expect(await collector.fetch(mockedFetchContext)).toMatchSnapshot({ concurrent_connections: 20, os: { load: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts index d6f40a2a6867f..48e4e0d99d3cd 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts @@ -21,6 +21,7 @@ import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerUiMetricUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ui_metric', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerUiMetricUsageCollector(usageCollectionMock, registerType, getUsageCollector) @@ -47,7 +48,7 @@ describe('telemetry_ui_metric', () => { }); test('if no savedObjectClient initialised, return undefined', async () => { - expect(await collector.fetch(callCluster)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); }); test('when savedObjectClient is initialised, return something', async () => { @@ -61,7 +62,7 @@ describe('telemetry_ui_metric', () => { ); getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -85,7 +86,7 @@ describe('telemetry_ui_metric', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ testAppName: [ { key: 'testKeyName1', value: 3 }, { key: 'testKeyName2', value: 5 }, diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 9140de316605c..ecf6aa0569bf7 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -30,7 +30,6 @@ export { export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder'; export { SavedObjectLoader, - createSavedObjectClass, checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal, diff --git a/src/plugins/saved_objects/public/mocks.ts b/src/plugins/saved_objects/public/mocks.ts new file mode 100644 index 0000000000000..d34a6ded7c8de --- /dev/null +++ b/src/plugins/saved_objects/public/mocks.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 { SavedObjectsStart } from './plugin'; + +const createStartContract = (): SavedObjectsStart => { + return { + SavedObjectClass: jest.fn(), + settings: { + getPerPage: () => 20, + getListingLimit: () => 100, + }, + }; +}; + +export const savedObjectsPluginMock = { + createStartContract, +}; diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index d430c8896484d..0c50180e13d86 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -23,9 +23,10 @@ import './index.scss'; import { createSavedObjectClass } from './saved_object'; import { DataPublicPluginStart } from '../../data/public'; import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; +import { SavedObject } from './types'; export interface SavedObjectsStart { - SavedObjectClass: any; + SavedObjectClass: new (raw: Record) => SavedObject; settings: { getPerPage: () => number; getListingLimit: () => number; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 47390c7dc9104..04fa3647de4c7 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -23,8 +23,8 @@ import { IndexPattern, injectSearchSourceReferences, parseSearchSourceJSON, - expandShorthand, } from '../../../../data/public'; +import { expandShorthand } from './field_mapping'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given diff --git a/src/plugins/data/common/field_mapping/index.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts similarity index 100% rename from src/plugins/data/common/field_mapping/index.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts diff --git a/src/plugins/data/common/field_mapping/mapping_setup.test.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts similarity index 96% rename from src/plugins/data/common/field_mapping/mapping_setup.test.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts index e57699e879a87..9353ff6796b5f 100644 --- a/src/plugins/data/common/field_mapping/mapping_setup.test.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts @@ -18,7 +18,7 @@ */ import { expandShorthand } from './mapping_setup'; -import { ES_FIELD_TYPES } from '../../../data/common'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; describe('mapping_setup', () => { it('allows shortcuts for field types by just setting the value to the type name', () => { diff --git a/src/plugins/data/common/field_mapping/mapping_setup.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts similarity index 96% rename from src/plugins/data/common/field_mapping/mapping_setup.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts index 0bad47d9889f0..804f03d345a96 100644 --- a/src/plugins/data/common/field_mapping/mapping_setup.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts @@ -21,7 +21,7 @@ import { mapValues, isString } from 'lodash'; import { FieldMappingSpec, MappingObject } from './types'; // import from ./common/types to prevent circular dependency of kibana_utils <-> data plugin -import { ES_FIELD_TYPES } from '../../../data/common/types'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; /** @private */ type ShorthandFieldMapObject = FieldMappingSpec | ES_FIELD_TYPES | 'json'; diff --git a/src/plugins/data/common/field_mapping/types.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts similarity index 94% rename from src/plugins/data/common/field_mapping/types.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts index 973a58d3baec4..a60a6b10623fc 100644 --- a/src/plugins/data/common/field_mapping/types.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ES_FIELD_TYPES } from '../../../data/common'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; /** @public */ export interface FieldMappingSpec { diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index 24e467ad18ac4..a0ab527ce1743 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -18,7 +18,8 @@ */ import _ from 'lodash'; import { SavedObject, SavedObjectConfig } from '../../types'; -import { extractSearchSourceReferences, expandShorthand } from '../../../../data/public'; +import { extractSearchSourceReferences } from '../../../../data/public'; +import { expandShorthand } from './field_mapping'; export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { // mapping definition for the fields that this object will expose diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx index f2eeedb5b7372..fff266bf964b0 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx @@ -32,7 +32,7 @@ import React, { useState } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', { - defaultMessage: 'Please secure your installation', + defaultMessage: 'Your data is not secure', }); export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountPoint = ( @@ -47,7 +47,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP @@ -66,7 +66,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP size="s" color="primary" fill - href="https://www.elastic.co/what-is/elastic-stack-security" + href="https://www.elastic.co/what-is/elastic-stack-security?blade=kibanasecuritymessage" target="_blank" > {i18n.translate('security.checkup.learnMoreButtonText', { diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 1f474dcbb8ff4..160e99a40790c 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1290,6 +1290,235 @@ } } }, + "core": { + "properties": { + "config": { + "properties": { + "elasticsearch": { + "properties": { + "sniffOnStart": { + "type": "boolean" + }, + "sniffIntervalMs": { + "type": "long" + }, + "sniffOnConnectionFault": { + "type": "boolean" + }, + "numberOfHostsConfigured": { + "type": "long" + }, + "requestHeadersWhitelistConfigured": { + "type": "boolean" + }, + "customHeadersConfigured": { + "type": "boolean" + }, + "shardTimeoutMs": { + "type": "long" + }, + "requestTimeoutMs": { + "type": "long" + }, + "pingTimeoutMs": { + "type": "long" + }, + "logQueries": { + "type": "boolean" + }, + "ssl": { + "properties": { + "verificationMode": { + "type": "keyword" + }, + "certificateAuthoritiesConfigured": { + "type": "boolean" + }, + "certificateConfigured": { + "type": "boolean" + }, + "keyConfigured": { + "type": "boolean" + }, + "keystoreConfigured": { + "type": "boolean" + }, + "truststoreConfigured": { + "type": "boolean" + }, + "alwaysPresentCertificate": { + "type": "boolean" + } + } + }, + "apiVersion": { + "type": "keyword" + }, + "healthCheckDelayMs": { + "type": "long" + } + } + }, + "http": { + "properties": { + "basePathConfigured": { + "type": "boolean" + }, + "maxPayloadInBytes": { + "type": "long" + }, + "rewriteBasePath": { + "type": "boolean" + }, + "keepaliveTimeout": { + "type": "long" + }, + "socketTimeout": { + "type": "long" + }, + "compression": { + "properties": { + "enabled": { + "type": "boolean" + }, + "referrerWhitelistConfigured": { + "type": "boolean" + } + } + }, + "xsrf": { + "properties": { + "disableProtection": { + "type": "boolean" + }, + "whitelistConfigured": { + "type": "boolean" + } + } + }, + "requestId": { + "properties": { + "allowFromAnyIp": { + "type": "boolean" + }, + "ipAllowlistConfigured": { + "type": "boolean" + } + } + }, + "ssl": { + "properties": { + "certificateAuthoritiesConfigured": { + "type": "boolean" + }, + "certificateConfigured": { + "type": "boolean" + }, + "cipherSuites": { + "type": "array", + "items": { + "type": "keyword" + } + }, + "keyConfigured": { + "type": "boolean" + }, + "keystoreConfigured": { + "type": "boolean" + }, + "truststoreConfigured": { + "type": "boolean" + }, + "redirectHttpFromPortConfigured": { + "type": "boolean" + }, + "supportedProtocols": { + "type": "array", + "items": { + "type": "keyword" + } + }, + "clientAuthentication": { + "type": "keyword" + } + } + } + } + }, + "logging": { + "properties": { + "appendersTypesUsed": { + "type": "array", + "items": { + "type": "keyword" + } + }, + "loggersConfiguredCount": { + "type": "long" + } + } + }, + "savedObjects": { + "properties": { + "maxImportPayloadBytes": { + "type": "long" + }, + "maxImportExportSizeBytes": { + "type": "long" + } + } + } + } + }, + "environment": { + "properties": { + "memory": { + "properties": { + "heapSizeLimit": { + "type": "long" + }, + "heapTotalBytes": { + "type": "long" + }, + "heapUsedBytes": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "savedObjects": { + "properties": { + "indices": { + "type": "array", + "items": { + "properties": { + "docsCount": { + "type": "long" + }, + "docsDeleted": { + "type": "long" + }, + "alias": { + "type": "text" + }, + "primaryStoreSizeBytes": { + "type": "long" + }, + "storeSizeBytes": { + "type": "long" + } + } + } + } + } + } + } + } + } + }, "csp": { "properties": { "strict": { @@ -2502,6 +2731,48 @@ "type": "long" } } + }, + "vis_type_vega": { + "properties": { + "vega_lib_specs_total": { + "type": "long" + }, + "vega_lite_lib_specs_total": { + "type": "long" + }, + "vega_use_map_total": { + "type": "long" + } + } + }, + "visualization_types": { + "properties": { + "DYNAMIC_KEY": { + "properties": { + "total": { + "type": "long" + }, + "spaces_min": { + "type": "long" + }, + "spaces_max": { + "type": "long" + }, + "spaces_avg": { + "type": "long" + }, + "saved_7_days_total": { + "type": "long" + }, + "saved_30_days_total": { + "type": "long" + }, + "saved_90_days_total": { + "type": "long" + } + } + } + } } } } diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index b423cbb07ba32..037f97fb63ac6 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -35,6 +35,7 @@ import { Logger, IClusterClient, UiSettingsServiceStart, + SavedObjectsServiceStart, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -88,6 +89,7 @@ export class TelemetryPlugin implements Plugin) { this.logger = initializerContext.logger.get(); @@ -110,7 +112,8 @@ export class TelemetryPlugin implements Plugin this.elasticsearchClient + () => this.elasticsearchClient, + () => this.savedObjectsService ); const router = http.createRouter(); @@ -139,6 +142,7 @@ export class TelemetryPlugin implements Plugin { - const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser); + const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser, soClient); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 0c8b0b249f7d1..fcecbca23038e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -20,7 +20,10 @@ import { merge, omit } from 'lodash'; import { getLocalStats, handleLocalStats } from './get_local_stats'; -import { usageCollectionPluginMock } from '../../../usage_collection/server/mocks'; +import { + usageCollectionPluginMock, + createCollectorFetchContextMock, +} from '../../../usage_collection/server/mocks'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; function mockUsageCollection(kibanaUsage = {}) { @@ -79,6 +82,16 @@ function mockGetLocalStats(clusterInfo: any, clusterStats: any) { return esClient; } +function mockStatsCollectionConfig(clusterInfo: any, clusterStats: any, kibana: {}) { + return { + ...createCollectorFetchContextMock(), + esClient: mockGetLocalStats(clusterInfo, clusterStats), + usageCollection: mockUsageCollection(kibana), + start: '', + end: '', + }; +} + describe('get_local_stats', () => { const clusterUuid = 'abc123'; const clusterName = 'my-cool-cluster'; @@ -224,12 +237,10 @@ describe('get_local_stats', () => { describe('getLocalStats', () => { it('returns expected object with kibana data', async () => { - const callCluster = jest.fn(); - const usageCollection = mockUsageCollection(kibana); - const esClient = mockGetLocalStats(clusterInfo, clusterStats); + const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); const response = await getLocalStats( [{ clusterUuid: 'abc123' }], - { callCluster, usageCollection, esClient, start: '', end: '' }, + { ...statsCollectionConfig }, context ); const result = response[0]; @@ -244,14 +255,8 @@ describe('get_local_stats', () => { }); it('returns an empty array when no cluster uuid is provided', async () => { - const callCluster = jest.fn(); - const usageCollection = mockUsageCollection(kibana); - const esClient = mockGetLocalStats(clusterInfo, clusterStats); - const response = await getLocalStats( - [], - { callCluster, usageCollection, esClient, start: '', end: '' }, - context - ); + const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); + const response = await getLocalStats([], { ...statsCollectionConfig }, context); expect(response).toBeDefined(); expect(response.length).toEqual(0); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 6244c6fac51d3..4aeefb1d81d6a 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -68,10 +68,10 @@ export type TelemetryLocalStats = ReturnType; */ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( clustersDetails, // array of cluster uuid's - config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end + config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end and the saved objects client scoped to the request or the internal repository context // StatsCollectionContext contains logger and version (string) ) => { - const { callCluster, usageCollection, esClient } = config; + const { callCluster, usageCollection, esClient, soClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -79,7 +79,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, callCluster, esClient), + getKibana(usageCollection, callCluster, esClient, soClient), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts index 9dac4900f5f10..27ca5ae746512 100644 --- a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -36,7 +36,7 @@ * under the License. */ -import { ILegacyClusterClient } from 'kibana/server'; +import { ILegacyClusterClient, SavedObjectsServiceStart } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { IClusterClient } from '../../../../../src/core/server'; import { getLocalStats } from './get_local_stats'; @@ -46,11 +46,13 @@ import { getLocalLicense } from './get_local_license'; export function registerCollection( telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, esCluster: ILegacyClusterClient, - esClientGetter: () => IClusterClient | undefined + esClientGetter: () => IClusterClient | undefined, + soServiceGetter: () => SavedObjectsServiceStart | undefined ) { telemetryCollectionManager.setCollection({ esCluster, esClientGetter, + soServiceGetter, title: 'local', priority: 0, statsGetter: getLocalStats, diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index ff63262004cf5..4900e75a1936b 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -25,6 +25,7 @@ import { Plugin, Logger, IClusterClient, + SavedObjectsServiceStart, } from '../../../core/server'; import { @@ -90,6 +91,7 @@ export class TelemetryCollectionManagerPlugin priority, esCluster, esClientGetter, + soServiceGetter, statsGetter, clusterDetailsGetter, licenseGetter, @@ -112,6 +114,9 @@ export class TelemetryCollectionManagerPlugin if (!esClientGetter) { throw Error('esClientGetter method not set.'); } + if (!soServiceGetter) { + throw Error('soServiceGetter method not set.'); + } if (!clusterDetailsGetter) { throw Error('Cluster UUIds method is not set.'); } @@ -126,6 +131,7 @@ export class TelemetryCollectionManagerPlugin esCluster, title, esClientGetter, + soServiceGetter, }); this.usageGetterMethodPriority = priority; } @@ -135,6 +141,7 @@ export class TelemetryCollectionManagerPlugin config: StatsGetterConfig, collection: Collection, collectionEsClient: IClusterClient, + collectionSoService: SavedObjectsServiceStart, usageCollection: UsageCollectionSetup ): StatsCollectionConfig { const { start, end, request } = config; @@ -146,7 +153,11 @@ export class TelemetryCollectionManagerPlugin const esClient = config.unencrypted ? collectionEsClient.asScoped(config.request).asCurrentUser : collectionEsClient.asInternalUser; - return { callCluster, start, end, usageCollection, esClient }; + // Scope the saved objects client appropriately and pass to the stats collection config + const soClient = config.unencrypted + ? collectionSoService.getScopedClient(config.request) + : collectionSoService.createInternalRepository(); + return { callCluster, start, end, usageCollection, esClient, soClient }; } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { @@ -156,11 +167,13 @@ export class TelemetryCollectionManagerPlugin for (const collection of this.collections) { // first fetch the client and make sure it's not undefined. const collectionEsClient = collection.esClientGetter(); - if (collectionEsClient !== undefined) { + const collectionSoService = collection.soServiceGetter(); + if (collectionEsClient !== undefined && collectionSoService !== undefined) { const statsCollectionConfig = this.getStatsCollectionConfig( config, collection, collectionEsClient, + collectionSoService, this.usageCollection ); @@ -215,11 +228,13 @@ export class TelemetryCollectionManagerPlugin } for (const collection of this.collections) { const collectionEsClient = collection.esClientGetter(); - if (collectionEsClient !== undefined) { + const collectionSavedObjectsService = collection.soServiceGetter(); + if (collectionEsClient !== undefined && collectionSavedObjectsService !== undefined) { const statsCollectionConfig = this.getStatsCollectionConfig( config, collection, collectionEsClient, + collectionSavedObjectsService, this.usageCollection ); try { diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 3b0936fb73a60..d6e4fdce2b188 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -23,6 +23,9 @@ import { KibanaRequest, ILegacyClusterClient, IClusterClient, + SavedObjectsServiceStart, + SavedObjectsClientContract, + ISavedObjectsRepository, } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ElasticsearchClient } from '../../../../src/core/server'; @@ -77,6 +80,7 @@ export interface StatsCollectionConfig { start: string | number; end: string | number; esClient: ElasticsearchClient; + soClient: SavedObjectsClientContract | ISavedObjectsRepository; } export interface BasicStatsPayload { @@ -141,6 +145,7 @@ export interface CollectionConfig< priority: number; esCluster: ILegacyClusterClient; esClientGetter: () => IClusterClient | undefined; // --> by now we know that the client getter will return the IClusterClient but we assure that through a code check + soServiceGetter: () => SavedObjectsServiceStart | undefined; // --> by now we know that the service getter will return the SavedObjectsServiceStart but we assure that through a code check statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; licenseGetter: LicenseGetter; @@ -157,5 +162,6 @@ export interface Collection< clusterDetailsGetter: ClusterDetailsGetter; esCluster: ILegacyClusterClient; esClientGetter: () => IClusterClient | undefined; // the collection could still return undefined for the es client getter. + soServiceGetter: () => SavedObjectsServiceStart | undefined; // the collection could still return undefined for the Saved Objects Service getter. title: string; } diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index d8c709d867a3f..3134cc265fba1 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -6,7 +6,6 @@ "requiredBundles": [ "kibanaLegacy", "kibanaUtils", - "savedObjects", "visTypeTimelion" ], "requiredPlugins": [ @@ -14,6 +13,7 @@ "data", "navigation", "visTypeTimelion", + "savedObjects", "kibanaLegacy" ] } diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts index a4963ee6b1b03..e0425ac94c59b 100644 --- a/src/plugins/timelion/public/application.ts +++ b/src/plugins/timelion/public/application.ts @@ -41,7 +41,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../kibana_legacy/public'; -import { TimelionPluginDependencies } from './plugin'; +import { TimelionPluginStartDependencies } from './plugin'; import { DataPublicPluginStart } from '../../data/public'; // @ts-ignore import { initTimelionApp } from './app'; @@ -50,7 +50,7 @@ export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; mountParams: AppMountParameters; core: CoreStart; - plugins: TimelionPluginDependencies; + plugins: TimelionPluginStartDependencies; timelionPanels: Map; } @@ -137,7 +137,7 @@ function createLocalIconModule() { .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) { +function createLocalTopNavModule(navigation: TimelionPluginStartDependencies['navigation']) { angular .module('app/timelion/TopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) diff --git a/src/plugins/timelion/public/flot/jquery.flot.js b/src/plugins/timelion/public/flot/jquery.flot.js deleted file mode 100644 index 5d613037cf234..0000000000000 --- a/src/plugins/timelion/public/flot/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/plugins/timelion/public/flot/jquery.flot.time.js b/src/plugins/timelion/public/flot/jquery.flot.time.js deleted file mode 100644 index 34c1d121259a2..0000000000000 --- a/src/plugins/timelion/public/flot/jquery.flot.time.js +++ /dev/null @@ -1,432 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - } - - if (dayNames == null) { - dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts index b56d8a66110c2..d874f0d32c9d4 100644 --- a/src/plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -17,7 +17,6 @@ * under the License. */ -import '../../flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 7656a808dfb00..b435cc6fd399b 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -36,20 +36,29 @@ import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { VisualizationsStart } from '../../visualizations/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; import { VisTypeTimelionPluginStart, VisTypeTimelionPluginSetup, } from '../../vis_type_timelion/public'; -export interface TimelionPluginDependencies { +export interface TimelionPluginSetupDependencies { + data: DataPublicPluginSetup; + visTypeTimelion: VisTypeTimelionPluginSetup; +} + +export interface TimelionPluginStartDependencies { data: DataPublicPluginStart; navigation: NavigationPublicPluginStart; visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; + savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ -export class TimelionPlugin implements Plugin { +export class TimelionPlugin + implements Plugin { initializerContext: PluginInitializerContext; private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; @@ -60,7 +69,7 @@ export class TimelionPlugin implements Plugin { } public setup( - core: CoreSetup, + core: CoreSetup, { data, visTypeTimelion, @@ -122,7 +131,7 @@ export class TimelionPlugin implements Plugin { pluginInitializerContext: this.initializerContext, timelionPanels, core: coreStart, - plugins: pluginsStart as TimelionPluginDependencies, + plugins: pluginsStart, }); return () => { unlistenParentHistory(); diff --git a/src/plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts index 0958cce860126..3fe66fabebe73 100644 --- a/src/plugins/timelion/public/services/_saved_sheet.ts +++ b/src/plugins/timelion/public/services/_saved_sheet.ts @@ -18,16 +18,11 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { createSavedObjectClass, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsStart } from '../../../saved_objects/public'; // Used only by the savedSheets service, usually no reason to change this -export function createSavedSheetClass( - services: SavedObjectKibanaServices, - config: IUiSettingsClient -) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSheet extends SavedObjectClass { +export function createSavedSheetClass(savedObjects: SavedObjectsStart, config: IUiSettingsClient) { + class SavedSheet extends savedObjects.SavedObjectClass { static type = 'timelion-sheet'; // if type:sheet has no mapping, we push this mapping into ES diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts index 9ad529cb0134b..4c360ad558234 100644 --- a/src/plugins/timelion/public/services/saved_sheets.ts +++ b/src/plugins/timelion/public/services/saved_sheets.ts @@ -23,15 +23,7 @@ import { RenderDeps } from '../application'; export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) { const savedObjectsClient = deps.core.savedObjects.client; - const services = { - savedObjectsClient, - indexPatterns: deps.plugins.data.indexPatterns, - search: deps.plugins.data.search, - chrome: deps.core.chrome, - overlays: deps.core.overlays, - }; - - const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings); + const SavedSheet = createSavedSheetClass(deps.plugins.savedObjects, deps.core.uiSettings); const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient); savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index aae633a956c48..430241cbe0a05 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -37,10 +37,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t ``` 3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. - ```ts // server/collectors/register.ts - import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { APICluster } from 'kibana/server'; interface Usage { @@ -63,9 +62,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, - fetch: async (callCluster: APICluster, esClient: IClusterClient) => { + fetch: async (collectorFetchContext: CollectorFetchContext) => { - // query ES and get some data + // query ES or saved objects and get some data // summarize the data into a model // return the modeled object that includes whatever you want to track @@ -86,9 +85,11 @@ Some background: - `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector. - The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. -In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. +In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. The `fetch` method also exposes the saved objects client that will have the correct scope when the collectors' `fetch` method is called. + +Note: there will be many cases where you won't need to use the `callCluster`, `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. -Note: there will be many cases where you won't need to use the `callCluster` (or `esClient`) function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below: +In the case of using a custom SavedObjects client, it is up to the plugin to initialize the client to save the data and it is strongly recommended to scope that client to the `kibana_system` user. ```ts // server/plugin.ts @@ -99,7 +100,7 @@ class Plugin { private savedObjectsRepository?: ISavedObjectsRepository; public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { - registerMyPluginUsageCollector(() => this.savedObjectsRepository, plugins.usageCollection); + registerMyPluginUsageCollector(plugins.usageCollection); } public start(core: CoreStart) { diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 8491bdb0c957c..11a709c037783 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -17,7 +17,13 @@ * under the License. */ -import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; +import { + Logger, + LegacyAPICaller, + ElasticsearchClient, + ISavedObjectsRepository, + SavedObjectsClientContract, +} from 'kibana/server'; export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; @@ -45,11 +51,30 @@ export type MakeSchemaFrom = { : RecursiveMakeSchemaFrom[Key]>; }; +export interface CollectorFetchContext { + /** + * @depricated Scoped Legacy Elasticsearch client: use esClient instead + */ + callCluster: LegacyAPICaller; + /** + * Request-scoped Elasticsearch client: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + */ + esClient: ElasticsearchClient; + /** + * Request-scoped Saved Objects client: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + */ + soClient: SavedObjectsClientContract | ISavedObjectsRepository; +} + export interface CollectorOptions { type: string; init?: Function; schema?: MakeSchemaFrom; - fetch: (callCluster: LegacyAPICaller, esClient?: ElasticsearchClient) => Promise | T; + fetch: (collectorFetchContext: CollectorFetchContext) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed * data model for internal bulk upload. See defaultFormatterForBulkUpload for diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 3f943ad8bf2ff..45a3437777c5f 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -21,7 +21,11 @@ import { noop } from 'lodash'; import { Collector } from './collector'; import { CollectorSet } from './collector_set'; import { UsageCollector } from './usage_collector'; -import { elasticsearchServiceMock, loggingSystemMock } from '../../../../core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsRepositoryMock, +} from '../../../../core/server/mocks'; const logger = loggingSystemMock.createLogger(); @@ -40,9 +44,9 @@ describe('CollectorSet', () => { loggerSpies.debug.mockRestore(); loggerSpies.warn.mockRestore(); }); - const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockSoClient = savedObjectsRepositoryMock.create(); it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet({ logger }); @@ -81,12 +85,14 @@ describe('CollectorSet', () => { collectors.registerCollector( new Collector(logger, { type: 'MY_TEST_COLLECTOR', - fetch: (caller: any) => caller(), + fetch: (collectorFetchContext: any) => { + return collectorFetchContext.callCluster(); + }, isReady: () => true, }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -111,7 +117,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); } catch (err) { // Do nothing } @@ -129,7 +135,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -147,7 +153,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -170,7 +176,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 7bf4e19c72cc0..4e64cbc1bf30f 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -18,7 +18,13 @@ */ import { snakeCase } from 'lodash'; -import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; +import { + Logger, + LegacyAPICaller, + ElasticsearchClient, + ISavedObjectsRepository, + SavedObjectsClientContract, +} from 'kibana/server'; import { Collector, CollectorOptions } from './collector'; import { UsageCollector } from './usage_collector'; @@ -122,12 +128,10 @@ export class CollectorSet { return allReady; }; - // all collections eventually pass through bulkFetch. - // the shape of the response is different when using the new ES client as is the error handling. - // We'll handle the refactor for using the new client in a follow up PR. public bulkFetch = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract | ISavedObjectsRepository, collectors: Map> = this.collectors ) => { const responses = await Promise.all( @@ -136,7 +140,7 @@ export class CollectorSet { try { return { type: collector.type, - result: await collector.fetch(callCluster, esClient), // each collector must ensure they handle the response appropriately. + result: await collector.fetch({ callCluster, esClient, soClient }), }; } catch (err) { this.logger.warn(err); @@ -158,9 +162,18 @@ export class CollectorSet { return this.makeCollectorSetFromArray(filtered); }; - public bulkFetchUsage = async (callCluster: LegacyAPICaller, esClient: ElasticsearchClient) => { + public bulkFetchUsage = async ( + callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository + ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch(callCluster, esClient, usageCollectors.collectors); + return await this.bulkFetch( + callCluster, + esClient, + savedObjectsClient, + usageCollectors.collectors + ); }; // convert an array of fetched stats results into key/object diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 1816e845b4d66..c294ba77d3cdb 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -24,5 +24,6 @@ export { SchemaField, MakeSchemaFrom, CollectorOptions, + CollectorFetchContext, } from './collector'; export { UsageCollector } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 87761bca9a507..80e34b1502cda 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -26,6 +26,7 @@ export { SchemaField, CollectorOptions, Collector, + CollectorFetchContext, } from './collector'; export { UsageCollectionSetup } from './plugin'; export { config } from './config'; diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index e1f13304165a1..d08db1eaec0e1 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -20,6 +20,7 @@ import { loggingSystemMock } from '../../../core/server/mocks'; import { UsageCollectionSetup } from './plugin'; import { CollectorSet } from './collector'; +export { createCollectorFetchContextMock } from './usage_collection.mock'; const createSetupContract = () => { return { diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index bee25fef669f1..ef64d15fabc2d 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -26,8 +26,10 @@ import { first } from 'rxjs/operators'; import { ElasticsearchClient, IRouter, + ISavedObjectsRepository, LegacyAPICaller, MetricsServiceSetup, + SavedObjectsClientContract, ServiceStatus, ServiceStatusLevels, } from '../../../../../core/server'; @@ -64,9 +66,10 @@ export function registerStatsRoute({ }) { const getUsage = async ( callCluster: LegacyAPICaller, - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository ): Promise => { - const usage = await collectorSet.bulkFetchUsage(callCluster, esClient); + const usage = await collectorSet.bulkFetchUsage(callCluster, esClient, savedObjectsClient); return collectorSet.toObject(usage); }; @@ -101,6 +104,7 @@ export function registerStatsRoute({ if (isExtended) { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.core.savedObjects.client; if (shouldGetUsage) { const collectorsReady = await collectorSet.areAllCollectorsReady(); @@ -109,7 +113,9 @@ export function registerStatsRoute({ } } - const usagePromise = shouldGetUsage ? getUsage(callCluster, esClient) : Promise.resolve({}); + const usagePromise = shouldGetUsage + ? getUsage(callCluster, esClient, savedObjectsClient) + : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); let modifiedUsage = usage; diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index 7a6d16d6950ec..c31756c60e32d 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -17,8 +17,13 @@ * under the License. */ +import { + elasticsearchServiceMock, + savedObjectsRepositoryMock, +} from '../../../../src/core/server/mocks'; + import { CollectorOptions } from './collector/collector'; -import { UsageCollectionSetup } from './index'; +import { UsageCollectionSetup, CollectorFetchContext } from './index'; export { CollectorOptions }; @@ -45,3 +50,12 @@ export const createUsageCollectionSetupMock = () => { usageCollectionSetupMock.areAllCollectorsReady.mockResolvedValue(true); return usageCollectionSetupMock; }; + +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { + callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + soClient: savedObjectsRepositoryMock.create(), + }; + return collectorFetchClientsMock; +} diff --git a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx index 4c843791153b0..f1497631b66c5 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx @@ -106,7 +106,7 @@ describe('OrderAggParamEditor component', () => { mount(); - expect(setValue).toHaveBeenCalledWith('agg5'); + expect(setValue).toHaveBeenCalledWith('agg3'); }); it('defaults to the _key metric if no agg is compatible', () => { diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 0efd6e7746fd2..707b14c23ea75 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,12 +22,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EventEmitter } from 'events'; import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; -import { EditorRenderProps } from 'src/plugins/visualize/public'; +import { EditorRenderProps, IEditorController } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; const DefaultEditor = lazy(() => import('./default_editor')); -class DefaultEditorController { +class DefaultEditorController implements IEditorController { constructor( private el: HTMLElement, private vis: Vis, diff --git a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap index dc6571de969f0..a32609c2e3d34 100644 --- a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap +++ b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "table_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "dimensions": Object { "buckets": Array [], diff --git a/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..d2298e6fb3eb5 --- /dev/null +++ b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis toExpressionAst function should match snapshot based on params & dimensions 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + true, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":20,\\"percentageCol\\":\\"Count\\",\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"showTotal\\":true,\\"sort\\":{\\"columnIndex\\":null,\\"direction\\":null},\\"totalFunc\\":\\"sum\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}", + ], + }, + "function": "kibana_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function should match snapshot without params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"showLabel\\":false,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}", + ], + }, + "function": "kibana_table", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index 5621fdb094772..6493c967165db 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import './index.scss'; import { PluginInitializerContext } from 'kibana/public'; import { TableVisPlugin as Plugin } from './plugin'; diff --git a/src/plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/legacy/_table_vis.scss similarity index 91% rename from src/plugins/vis_type_table/public/_table_vis.scss rename to src/plugins/vis_type_table/public/legacy/_table_vis.scss index 8a36b9818c2a3..fa12ef9a1cf39 100644 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ b/src/plugins/vis_type_table/public/legacy/_table_vis.scss @@ -4,8 +4,10 @@ .table-vis { display: flex; flex-direction: column; - flex: 1 0 100%; + flex: 1 1 0; overflow: auto; + + @include euiScrollBar; } .table-vis-container { diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/_agg_table.scss rename to src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_index.scss similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/_index.scss rename to src/plugins/vis_type_table/public/legacy/agg_table/_index.scss diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table.html rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js similarity index 99% rename from src/plugins/vis_type_table/public/agg_table/agg_table.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js index 1e98a06c2a6a9..a9ec431e9d940 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js @@ -17,9 +17,9 @@ * under the License. */ import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; +import { getFormatService } from '../../services'; import { i18n } from '@kbn/i18n'; export function KbnAggTable(config, RecursionHelper) { diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js similarity index 97% rename from src/plugins/vis_type_table/public/agg_table/agg_table.test.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js index 29a10151a9418..c93fb4f8bd568 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js @@ -24,14 +24,14 @@ import 'angular-mocks'; import sinon from 'sinon'; import { round } from 'lodash'; -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../kibana_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { UI_SETTINGS } from '../../../data/public/'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; - -import { setFormatService } from '../services'; +import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; +import { coreMock } from '../../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { setUiSettings } from '../../../../data/public/services'; +import { UI_SETTINGS } from '../../../../data/public/'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; + +import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.html rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js similarity index 92% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js index 04cf624331d81..833f51b446ac1 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js @@ -22,11 +22,11 @@ import angular from 'angular'; import 'angular-mocks'; import expect from '@kbn/expect'; -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../kibana_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { setFormatService } from '../services'; +import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; +import { coreMock } from '../../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { setUiSettings } from '../../../../data/public/services'; +import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; diff --git a/src/plugins/vis_type_table/public/agg_table/tabified_data.js b/src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/tabified_data.js rename to src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts similarity index 98% rename from src/plugins/vis_type_table/public/get_inner_angular.ts rename to src/plugins/vis_type_table/public/legacy/get_inner_angular.ts index 4e4269a1f44f4..f3d88a2a217b3 100644 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts @@ -33,7 +33,7 @@ import { PrivateProvider, watchMultiDecorator, KbnAccessibleClickProvider, -} from '../../kibana_legacy/public'; +} from '../../../kibana_legacy/public'; initAngularBootstrap(); diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/legacy/index.scss similarity index 100% rename from src/plugins/vis_type_table/public/index.scss rename to src/plugins/vis_type_table/public/legacy/index.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/_index.scss rename to src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss rename to src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.html rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.js rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts similarity index 98% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts index de253f26ff9e7..3bc5f79557041 100644 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts @@ -25,11 +25,7 @@ import 'angular-mocks'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../core/public/mocks'; - -jest.mock('../../../kibana_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); +import { coreMock } from '../../../../../core/public/mocks'; interface Sort { columnIndex: number; diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js similarity index 91% rename from src/plugins/vis_type_table/public/paginated_table/rows.js rename to src/plugins/vis_type_table/public/legacy/paginated_table/rows.js index d8f01a10c63fa..54e754adcc170 100644 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js @@ -44,15 +44,18 @@ export function KbnRows($compile) { } $scope.filter({ - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, + name: 'filterBucket', + data: { + data: [ + { + table: $scope.table, + row: $scope.rows.findIndex((r) => r === row), + column: $scope.table.columns.findIndex((c) => c.id === column.id), + value, + }, + ], + negate, + }, }); }; diff --git a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html rename to src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html diff --git a/src/plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/legacy/table_vis.html similarity index 96% rename from src/plugins/vis_type_table/public/table_vis.html rename to src/plugins/vis_type_table/public/legacy/table_vis.html index f721b670400d6..c469cd250755c 100644 --- a/src/plugins/vis_type_table/public/table_vis.html +++ b/src/plugins/vis_type_table/public/legacy/table_vis.html @@ -15,7 +15,7 @@
({ - configureAppAngularModule: () => {}, -})); - interface TableVisScope extends IScope { [key: string]: any; } @@ -112,10 +107,6 @@ describe('Table Vis - Controller', () => { coreMock.createSetup() ); }); - const tableVisTypeDefinition = getTableVisTypeDefinition( - coreMock.createSetup(), - coreMock.createPluginInitializerContext() - ); function getRangeVis(params?: object) { return ({ diff --git a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts similarity index 100% rename from src/plugins/vis_type_table/public/table_vis_legacy_module.ts rename to src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx new file mode 100644 index 0000000000000..ab633bd5137ba --- /dev/null +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx @@ -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 { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { TablePluginStartDependencies } from '../plugin'; +import { TableVisRenderValue } from '../table_vis_fn'; +import { TableVisLegacyController } from './vis_controller'; + +const tableVisRegistry = new Map(); + +export const getTableVisLegacyRenderer: ( + core: CoreSetup, + context: PluginInitializerContext +) => ExpressionRenderDefinition = (core, context) => ({ + name: 'table_vis', + reuseDomNode: true, + render: async (domNode, config, handlers) => { + let registeredController = tableVisRegistry.get(domNode); + + if (!registeredController) { + const { getTableVisualizationControllerClass } = await import('./vis_controller'); + + const Controller = getTableVisualizationControllerClass(core, context); + registeredController = new Controller(domNode); + tableVisRegistry.set(domNode, registeredController); + + handlers.onDestroy(() => { + registeredController?.destroy(); + tableVisRegistry.delete(domNode); + }); + } + + await registeredController.render(config.visData, config.visConfig, handlers); + handlers.done(); + }, +}); diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts similarity index 68% rename from src/plugins/vis_type_table/public/vis_controller.ts rename to src/plugins/vis_type_table/public/legacy/vis_controller.ts index 1781808660260..eff8e34f3e778 100644 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -20,35 +20,43 @@ import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; import $ from 'jquery'; -import { VisParams, ExprVis } from '../../visualizations/public'; +import './index.scss'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { getAngularModule } from './get_inner_angular'; -import { getKibanaLegacy } from './services'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; +// @ts-ignore +import tableVisTemplate from './table_vis.html'; +import { TablePluginStartDependencies } from '../plugin'; +import { TableVisConfig } from '../types'; +import { TableContext } from '../table_vis_response_handler'; const innerAngularName = 'kibana/table_vis'; +export type TableVisLegacyController = InstanceType< + ReturnType +>; + export function getTableVisualizationControllerClass( - core: CoreSetup, + core: CoreSetup, context: PluginInitializerContext ) { return class TableVisualizationController { private tableVisModule: IModule | undefined; private injector: auto.IInjectorService | undefined; el: JQuery; - vis: ExprVis; $rootScope: IRootScopeService | null = null; $scope: (IScope & { [key: string]: any }) | undefined; $compile: ICompileService | undefined; - constructor(domeElement: Element, vis: ExprVis) { + constructor(domeElement: Element) { this.el = $(domeElement); - this.vis = vis; } getInjector() { if (!this.injector) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); + mountpoint.className = 'visualization'; this.injector = angular.bootstrap(mountpoint, [innerAngularName]); this.el.append(mountpoint); } @@ -58,14 +66,18 @@ export function getTableVisualizationControllerClass( async initLocalAngular() { if (!this.tableVisModule) { - const [coreStart] = await core.getStartServices(); + const [coreStart, { kibanaLegacy }] = await core.getStartServices(); this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); initTableVisLegacyModule(this.tableVisModule); + kibanaLegacy.loadFontAwesome(); } } - async render(esResponse: object, visParams: VisParams): Promise { - getKibanaLegacy().loadFontAwesome(); + async render( + esResponse: TableContext, + visParams: TableVisConfig, + handlers: IInterpreterRenderHandlers + ): Promise { await this.initLocalAngular(); return new Promise(async (resolve, reject) => { @@ -79,16 +91,6 @@ export function getTableVisualizationControllerClass( return; } - // How things get into this $scope? - // To inject variables into this $scope there's the following pipeline of stuff to check: - // - visualize_embeddable => that's what the editor creates to wrap this Angular component - // - build_pipeline => it serialize all the params into an Angular template compiled on the fly - // - table_vis_fn => unserialize the params and prepare them for the final React/Angular bridge - // - visualization_renderer => creates the wrapper component for this controller and passes the params - // - // In case some prop is missing check into the top of the chain if they are available and check - // the list above that it is passing through - this.$scope.vis = this.vis; this.$scope.visState = { params: visParams, title: visParams.title }; this.$scope.esResponse = esResponse; @@ -101,11 +103,10 @@ export function getTableVisualizationControllerClass( if (!this.$scope && this.$compile) { this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); + this.$scope.uiState = handlers.uiState; + this.$scope.filter = handlers.event; updateScope(); - this.el - .find('div') - .append(this.$compile(this.vis.type.visConfig?.template ?? '')(this.$scope)); + this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); this.$scope.$apply(); } else { updateScope(); diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 28f823df79d91..35ef5fc831cb7 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -21,10 +21,11 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { createTableVisFn } from './table_vis_fn'; -import { getTableVisTypeDefinition } from './table_vis_type'; +import { tableVisTypeDefinition } from './table_vis_type'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setKibanaLegacy } from './services'; +import { setFormatService } from './services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { getTableVisLegacyRenderer } from './legacy/table_vis_legacy_renderer'; /** @internal */ export interface TablePluginSetupDependencies { @@ -39,7 +40,9 @@ export interface TablePluginStartDependencies { } /** @internal */ -export class TableVisPlugin implements Plugin, void> { +export class TableVisPlugin + implements + Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { initializerContext: PluginInitializerContext; createBaseVisualization: any; @@ -48,17 +51,15 @@ export class TableVisPlugin implements Plugin, void> { } public async setup( - core: CoreSetup, + core: CoreSetup, { expressions, visualizations }: TablePluginSetupDependencies ) { expressions.registerFunction(createTableVisFn); - visualizations.createBaseVisualization( - getTableVisTypeDefinition(core, this.initializerContext) - ); + expressions.registerRenderer(getTableVisLegacyRenderer(core, this.initializerContext)); + visualizations.createBaseVisualization(tableVisTypeDefinition); } - public start(core: CoreStart, { data, kibanaLegacy }: TablePluginStartDependencies) { + public start(core: CoreStart, { data }: TablePluginStartDependencies) { setFormatService(data.fieldFormats); - setKibanaLegacy(kibanaLegacy); } } diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts index b4f996f078f6b..3aaffe75e27f1 100644 --- a/src/plugins/vis_type_table/public/services.ts +++ b/src/plugins/vis_type_table/public/services.ts @@ -19,12 +19,7 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('table data.fieldFormats'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'table kibanaLegacy' -); diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index 9739a7a284e6c..2e446ba4e4fcf 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { TableVisConfig } from './types'; export type Input = KibanaDatatable; @@ -27,23 +28,20 @@ interface Arguments { visConfig: string | null; } -type VisParams = Required; - -interface RenderValue { +export interface TableVisRenderValue { visData: TableContext; visType: 'table'; - visConfig: VisParams; - params: { - listenOnChange: boolean; - }; + visConfig: TableVisConfig; } -export const createTableVisFn = (): ExpressionFunctionDefinition< +export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition< 'kibana_table', Input, Arguments, - Render -> => ({ + Render +>; + +export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ name: 'kibana_table', type: 'render', inputTypes: ['kibana_datatable'], @@ -63,14 +61,11 @@ export const createTableVisFn = (): ExpressionFunctionDefinition< return { type: 'render', - as: 'visualization', + as: 'table_vis', value: { visData: convertedData, visType: 'table', visConfig, - params: { - listenOnChange: true, - }, }, }; }, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 95f4f06ee6111..bfc7abac02895 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -16,91 +16,82 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; + import { TableOptions } from './components/table_vis_options_lazy'; -import { getTableVisualizationControllerClass } from './vis_controller'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { TableVisParams } from './types'; -export function getTableVisTypeDefinition( - core: CoreSetup, - context: PluginInitializerContext -): BaseVisTypeOptions { - return { - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: getTableVisualizationControllerClass(core, context), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', +export const tableVisTypeDefinition: BaseVisTypeOptions = { + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, }, - template: tableVisTemplate, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, - }; -} + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + toExpressionAst, + hierarchicalData: (vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, +}; diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts new file mode 100644 index 0000000000000..045b5814944b0 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Vis } from 'src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { AggTypes, TableVisParams } from './types'; + +const mockSchemas = { + metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], + bucket: [ + { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: {}, + label: 'order_date per 3 hours', + aggType: 'date_histogram', + }, + ], +}; + +jest.mock('../../visualizations/public', () => ({ + getVisSchemas: () => mockSchemas, +})); + +describe('table vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: { + showLabel: false, + }, + data: { + indexPattern: { id: '123' }, + aggs: { + getResponseAggs: () => [], + aggs: [], + }, + }, + } as any; + }); + + it('should match snapshot without params', () => { + const actual = toExpressionAst(vis, {} as any); + expect(actual).toMatchSnapshot(); + }); + + it('should match snapshot based on params & dimensions', () => { + vis.params = { + perPage: 20, + percentageCol: 'Count', + showMetricsAtAllLevels: true, + showPartialRows: true, + showTotal: true, + sort: { columnIndex: null, direction: null }, + totalFunc: AggTypes.SUM, + }; + const actual = toExpressionAst(vis, {} as any); + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts new file mode 100644 index 0000000000000..449e2dde7f7c9 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EsaggsExpressionFunctionDefinition } from '../../data/common/search/expressions'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TableExpressionFunctionDefinition } from './table_vis_fn'; +import { TableVisConfig, TableVisParams } from './types'; + +const buildTableVisConfig = ( + schemas: ReturnType, + visParams: TableVisParams +) => { + const visConfig = {} as any; + const metrics = schemas.metric; + const buckets = schemas.bucket || []; + visConfig.dimensions = { + metrics, + buckets, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + if (visParams.showPartialRows && !visParams.showMetricsAtAllLevels) { + // Handle case where user wants to see partial rows but not metrics at all levels. + // This requires calculating how many metrics will come back in the tabified response, + // and removing all metrics from the dimensions except the last set. + const metricsPerBucket = metrics.length / buckets.length; + visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); + } + return visConfig; +}; + +export const toExpressionAst = (vis: Vis, params: BuildPipelineParams) => { + const esaggs = buildExpressionFunction('esaggs', { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: vis.params.showPartialRows, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + }); + + const schemas = getVisSchemas(vis, params); + + const visConfig: TableVisConfig = { + ...vis.params, + ...buildTableVisConfig(schemas, vis.params), + title: vis.title, + }; + + const table = buildExpressionFunction('kibana_table', { + visConfig: JSON.stringify(visConfig), + }); + + const ast = buildExpression([esaggs, table]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 39023d1305cb6..c0a995ad5da69 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -33,7 +33,6 @@ export interface Dimensions { } export interface TableVisParams { - type: 'table'; perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; @@ -44,5 +43,9 @@ export interface TableVisParams { showTotal: boolean; totalFunc: AggTypes; percentageCol: string; +} + +export interface TableVisConfig extends TableVisParams { + title: string; dimensions: Dimensions; } diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index a7b623ac8680c..953ec5e819f44 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -25,7 +25,6 @@ import { useResizeObserver } from '@elastic/eui'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { useKibana } from '../../../kibana_react/public'; -import '../flot'; import { DEFAULT_TIME_FORMAT } from '../../common/lib'; import { diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js deleted file mode 100644 index cda8038953c76..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js +++ /dev/null @@ -1,462 +0,0 @@ -/* -Axis Labels Plugin for flot. -http://github.com/markrcote/flot-axislabels -Original code is Copyright (c) 2010 Xuan Luo. -Original code was released under the GPLv3 license by Xuan Luo, September 2010. -Original code was rereleased under the MIT license by Xuan Luo, April 2012. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -(function ($) { - var options = { - axisLabels: { - show: true - } - }; - - function canvasSupported() { - return !!document.createElement('canvas').getContext; - } - - function canvasTextSupported() { - if (!canvasSupported()) { - return false; - } - var dummy_canvas = document.createElement('canvas'); - var context = dummy_canvas.getContext('2d'); - return typeof context.fillText == 'function'; - } - - function css3TransitionSupported() { - var div = document.createElement('div'); - return typeof div.style.MozTransition != 'undefined' // Gecko - || typeof div.style.OTransition != 'undefined' // Opera - || typeof div.style.webkitTransition != 'undefined' // WebKit - || typeof div.style.transition != 'undefined'; - } - - - function AxisLabel(axisName, position, padding, plot, opts) { - this.axisName = axisName; - this.position = position; - this.padding = padding; - this.plot = plot; - this.opts = opts; - this.width = 0; - this.height = 0; - } - - AxisLabel.prototype.cleanup = function() { - }; - - - CanvasAxisLabel.prototype = new AxisLabel(); - CanvasAxisLabel.prototype.constructor = CanvasAxisLabel; - function CanvasAxisLabel(axisName, position, padding, plot, opts) { - AxisLabel.prototype.constructor.call(this, axisName, position, padding, - plot, opts); - } - - CanvasAxisLabel.prototype.calculateSize = function() { - if (!this.opts.axisLabelFontSizePixels) - this.opts.axisLabelFontSizePixels = 14; - if (!this.opts.axisLabelFontFamily) - this.opts.axisLabelFontFamily = 'sans-serif'; - - var textWidth = this.opts.axisLabelFontSizePixels + this.padding; - var textHeight = this.opts.axisLabelFontSizePixels + this.padding; - if (this.position == 'left' || this.position == 'right') { - this.width = this.opts.axisLabelFontSizePixels + this.padding; - this.height = 0; - } else { - this.width = 0; - this.height = this.opts.axisLabelFontSizePixels + this.padding; - } - }; - - CanvasAxisLabel.prototype.draw = function(box) { - if (!this.opts.axisLabelColour) - this.opts.axisLabelColour = 'black'; - var ctx = this.plot.getCanvas().getContext('2d'); - ctx.save(); - ctx.font = this.opts.axisLabelFontSizePixels + 'px ' + - this.opts.axisLabelFontFamily; - ctx.fillStyle = this.opts.axisLabelColour; - var width = ctx.measureText(this.opts.axisLabel).width; - var height = this.opts.axisLabelFontSizePixels; - var x, y, angle = 0; - if (this.position == 'top') { - x = box.left + box.width/2 - width/2; - y = box.top + height*0.72; - } else if (this.position == 'bottom') { - x = box.left + box.width/2 - width/2; - y = box.top + box.height - height*0.72; - } else if (this.position == 'left') { - x = box.left + height*0.72; - y = box.height/2 + box.top + width/2; - angle = -Math.PI/2; - } else if (this.position == 'right') { - x = box.left + box.width - height*0.72; - y = box.height/2 + box.top - width/2; - angle = Math.PI/2; - } - ctx.translate(x, y); - ctx.rotate(angle); - ctx.fillText(this.opts.axisLabel, 0, 0); - ctx.restore(); - }; - - - HtmlAxisLabel.prototype = new AxisLabel(); - HtmlAxisLabel.prototype.constructor = HtmlAxisLabel; - function HtmlAxisLabel(axisName, position, padding, plot, opts) { - AxisLabel.prototype.constructor.call(this, axisName, position, - padding, plot, opts); - this.elem = null; - } - - HtmlAxisLabel.prototype.calculateSize = function() { - var elem = $('
' + - this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(elem); - // store height and width of label itself, for use in draw() - this.labelWidth = elem.outerWidth(true); - this.labelHeight = elem.outerHeight(true); - elem.remove(); - - this.width = this.height = 0; - if (this.position == 'left' || this.position == 'right') { - this.width = this.labelWidth + this.padding; - } else { - this.height = this.labelHeight + this.padding; - } - }; - - HtmlAxisLabel.prototype.cleanup = function() { - if (this.elem) { - this.elem.remove(); - } - }; - - HtmlAxisLabel.prototype.draw = function(box) { - this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); - this.elem = $('
' - + this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(this.elem); - if (this.position == 'top') { - this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + - 'px'); - this.elem.css('top', box.top + 'px'); - } else if (this.position == 'bottom') { - this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + - 'px'); - this.elem.css('top', box.top + box.height - this.labelHeight + - 'px'); - } else if (this.position == 'left') { - this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + - 'px'); - this.elem.css('left', box.left + 'px'); - } else if (this.position == 'right') { - this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + - 'px'); - this.elem.css('left', box.left + box.width - this.labelWidth + - 'px'); - } - }; - - - CssTransformAxisLabel.prototype = new HtmlAxisLabel(); - CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; - function CssTransformAxisLabel(axisName, position, padding, plot, opts) { - HtmlAxisLabel.prototype.constructor.call(this, axisName, position, - padding, plot, opts); - } - - CssTransformAxisLabel.prototype.calculateSize = function() { - HtmlAxisLabel.prototype.calculateSize.call(this); - this.width = this.height = 0; - if (this.position == 'left' || this.position == 'right') { - this.width = this.labelHeight + this.padding; - } else { - this.height = this.labelHeight + this.padding; - } - }; - - CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { - var stransforms = { - '-moz-transform': '', - '-webkit-transform': '', - '-o-transform': '', - '-ms-transform': '' - }; - if (x != 0 || y != 0) { - var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; - stransforms['-moz-transform'] += stdTranslate; - stransforms['-webkit-transform'] += stdTranslate; - stransforms['-o-transform'] += stdTranslate; - stransforms['-ms-transform'] += stdTranslate; - } - if (degrees != 0) { - var rotation = degrees / 90; - var stdRotate = ' rotate(' + degrees + 'deg)'; - stransforms['-moz-transform'] += stdRotate; - stransforms['-webkit-transform'] += stdRotate; - stransforms['-o-transform'] += stdRotate; - stransforms['-ms-transform'] += stdRotate; - } - var s = 'top: 0; left: 0; '; - for (var prop in stransforms) { - if (stransforms[prop]) { - s += prop + ':' + stransforms[prop] + ';'; - } - } - s += ';'; - return s; - }; - - CssTransformAxisLabel.prototype.calculateOffsets = function(box) { - var offsets = { x: 0, y: 0, degrees: 0 }; - if (this.position == 'bottom') { - offsets.x = box.left + box.width/2 - this.labelWidth/2; - offsets.y = box.top + box.height - this.labelHeight; - } else if (this.position == 'top') { - offsets.x = box.left + box.width/2 - this.labelWidth/2; - offsets.y = box.top; - } else if (this.position == 'left') { - offsets.degrees = -90; - offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; - offsets.y = box.height/2 + box.top; - } else if (this.position == 'right') { - offsets.degrees = 90; - offsets.x = box.left + box.width - this.labelWidth/2 - - this.labelHeight/2; - offsets.y = box.height/2 + box.top; - } - offsets.x = Math.round(offsets.x); - offsets.y = Math.round(offsets.y); - - return offsets; - }; - - CssTransformAxisLabel.prototype.draw = function(box) { - this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); - var offsets = this.calculateOffsets(box); - this.elem = $('
' + this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(this.elem); - }; - - - IeTransformAxisLabel.prototype = new CssTransformAxisLabel(); - IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel; - function IeTransformAxisLabel(axisName, position, padding, plot, opts) { - CssTransformAxisLabel.prototype.constructor.call(this, axisName, - position, padding, - plot, opts); - this.requiresResize = false; - } - - IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) { - // I didn't feel like learning the crazy Matrix stuff, so this uses - // a combination of the rotation transform and CSS positioning. - var s = ''; - if (degrees != 0) { - var rotation = degrees/90; - while (rotation < 0) { - rotation += 4; - } - s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); '; - // see below - this.requiresResize = (this.position == 'right'); - } - if (x != 0) { - s += 'left: ' + x + 'px; '; - } - if (y != 0) { - s += 'top: ' + y + 'px; '; - } - return s; - }; - - IeTransformAxisLabel.prototype.calculateOffsets = function(box) { - var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call( - this, box); - // adjust some values to take into account differences between - // CSS and IE rotations. - if (this.position == 'top') { - // FIXME: not sure why, but placing this exactly at the top causes - // the top axis label to flip to the bottom... - offsets.y = box.top + 1; - } else if (this.position == 'left') { - offsets.x = box.left; - offsets.y = box.height/2 + box.top - this.labelWidth/2; - } else if (this.position == 'right') { - offsets.x = box.left + box.width - this.labelHeight; - offsets.y = box.height/2 + box.top - this.labelWidth/2; - } - return offsets; - }; - - IeTransformAxisLabel.prototype.draw = function(box) { - CssTransformAxisLabel.prototype.draw.call(this, box); - if (this.requiresResize) { - this.elem = this.plot.getPlaceholder().find("." + this.axisName + - "Label"); - // Since we used CSS positioning instead of transforms for - // translating the element, and since the positioning is done - // before any rotations, we have to reset the width and height - // in case the browser wrapped the text (specifically for the - // y2axis). - this.elem.css('width', this.labelWidth); - this.elem.css('height', this.labelHeight); - } - }; - - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - - if (!options.axisLabels.show) - return; - - // This is kind of a hack. There are no hooks in Flot between - // the creation and measuring of the ticks (setTicks, measureTickLabels - // in setupGrid() ) and the drawing of the ticks and plot box - // (insertAxisLabels in setupGrid() ). - // - // Therefore, we use a trick where we run the draw routine twice: - // the first time to get the tick measurements, so that we can change - // them, and then have it draw it again. - var secondPass = false; - - var axisLabels = {}; - var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 }; - - var defaultPadding = 2; // padding between axis and tick labels - plot.hooks.draw.push(function (plot, ctx) { - var hasAxisLabels = false; - if (!secondPass) { - // MEASURE AND SET OPTIONS - $.each(plot.getAxes(), function(axisName, axis) { - var opts = axis.options // Flot 0.7 - || plot.getOptions()[axisName]; // Flot 0.6 - - // Handle redraws initiated outside of this plug-in. - if (axisName in axisLabels) { - axis.labelHeight = axis.labelHeight - - axisLabels[axisName].height; - axis.labelWidth = axis.labelWidth - - axisLabels[axisName].width; - opts.labelHeight = axis.labelHeight; - opts.labelWidth = axis.labelWidth; - axisLabels[axisName].cleanup(); - delete axisLabels[axisName]; - } - - if (!opts || !opts.axisLabel || !axis.show) - return; - - hasAxisLabels = true; - var renderer = null; - - if (!opts.axisLabelUseHtml && - navigator.appName == 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); - if (re.exec(ua) != null) { - rv = parseFloat(RegExp.$1); - } - if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { - renderer = CssTransformAxisLabel; - } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { - renderer = IeTransformAxisLabel; - } else if (opts.axisLabelUseCanvas) { - renderer = CanvasAxisLabel; - } else { - renderer = HtmlAxisLabel; - } - } else { - if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) { - renderer = HtmlAxisLabel; - } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) { - renderer = CanvasAxisLabel; - } else { - renderer = CssTransformAxisLabel; - } - } - - var padding = opts.axisLabelPadding === undefined ? - defaultPadding : opts.axisLabelPadding; - - axisLabels[axisName] = new renderer(axisName, - axis.position, padding, - plot, opts); - - // flot interprets axis.labelHeight and .labelWidth as - // the height and width of the tick labels. We increase - // these values to make room for the axis label and - // padding. - - axisLabels[axisName].calculateSize(); - - // AxisLabel.height and .width are the size of the - // axis label and padding. - // Just set opts here because axis will be sorted out on - // the redraw. - - opts.labelHeight = axis.labelHeight + - axisLabels[axisName].height; - opts.labelWidth = axis.labelWidth + - axisLabels[axisName].width; - }); - - // If there are axis labels, re-draw with new label widths and - // heights. - - if (hasAxisLabels) { - secondPass = true; - plot.setupGrid(); - plot.draw(); - } - } else { - secondPass = false; - // DRAW - $.each(plot.getAxes(), function(axisName, axis) { - var opts = axis.options // Flot 0.7 - || plot.getOptions()[axisName]; // Flot 0.6 - if (!opts || !opts.axisLabel || !axis.show) - return; - - axisLabels[axisName].draw(axis.box); - }); - } - }); - }); - } - - - $.plot.plugins.push({ - init: init, - options: options, - name: 'axisLabels', - version: '2.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.js deleted file mode 100644 index 5d613037cf234..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6f..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c0160..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js deleted file mode 100644 index 34c1d121259a2..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js +++ /dev/null @@ -1,432 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - } - - if (dayNames == null) { - dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/plugins/vis_type_timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts index ea08310499a96..242be515e52bc 100644 --- a/src/plugins/vis_type_timelion/server/routes/validate_es.ts +++ b/src/plugins/vis_type_timelion/server/routes/validate_es.ts @@ -57,10 +57,17 @@ export function validateEsRoute(router: IRouter, core: CoreSetup) { let resp; try { - resp = await deps.data.search.search(context, body, { - strategy: ES_SEARCH_STRATEGY, - }); - resp = resp.rawResponse; + resp = ( + await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + context + ) + .toPromise() + ).rawResponse; } catch (errResp) { resp = errResp; } diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index c5fc4b7b93269..8be3cf5171c65 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import es from './index'; - import tlConfigFn from '../fixtures/tl_config'; import * as aggResponse from './lib/agg_response_to_series_list'; import buildRequest from './lib/build_request'; @@ -36,7 +36,10 @@ function stubRequestAndServer(response, indexPatternSavedObjects = []) { getStartServices: sinon .stub() .returns( - Promise.resolve([{}, { data: { search: { search: () => Promise.resolve(response) } } }]) + Promise.resolve([ + {}, + { data: { search: { search: () => from(Promise.resolve(response)) } } }, + ]) ), savedObjectsClient: { find: function () { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js index bfa8d75900d11..fc3250f0d4726 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js @@ -132,9 +132,15 @@ export default new Datasource('es', { const deps = (await tlConfig.getStartServices())[1]; - const resp = await deps.data.search.search(tlConfig.context, body, { - strategy: ES_SEARCH_STRATEGY, - }); + const resp = await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + tlConfig.context + ) + .toPromise(); if (!resp.rawResponse._shards.total) { throw new Error( diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index b33215934c5df..40f776050617e 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -104,6 +104,7 @@ export const metricsItems = schema.object({ }) ) ), + numberOfSignificantValueDigits: numberOptional, percentiles: schema.maybe( schema.arrayOf( schema.object({ @@ -164,6 +165,7 @@ export const seriesItems = schema.object({ hide_in_legend: numberIntegerOptional, hidden: schema.maybe(schema.boolean()), id: stringRequired, + ignore_global_filter: numberOptional, label: stringOptionalNullable, line_width: numberOptionalOrEmptyString, metrics: schema.arrayOf(metricsItems), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index f12c0c8f6f465..a34b74d106492 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -24,10 +24,11 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { createNumberHandler } from '../lib/create_number_handler'; import { htmlIdGenerator, EuiSpacer, - EuiFlexGroup, + EuiFlexGrid, EuiFlexItem, EuiFormLabel, EuiFormRow, @@ -35,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; import { Percentiles, newPercentile } from './percentile_ui'; +import { PercentileHdr } from './percentile_hdr'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; @@ -46,6 +48,8 @@ export function PercentileAgg(props) { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); + const indexPattern = (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; @@ -66,7 +70,7 @@ export function PercentileAgg(props) { siblings={props.siblings} dragHandleProps={props.dragHandleProps} > - + - - - - - + + + } + > + + + + + + + ); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx new file mode 100644 index 0000000000000..c0ba7d3c6765b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx @@ -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 React from 'react'; +import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface PercentileHdrProps { + value: number | undefined; + onChange: () => void; +} + +export const PercentileHdr = ({ value, onChange }: PercentileHdrProps) => ( + + {' '} + + } + type="questionInCircle" + /> + + } + > + + +); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx index ef8876a19b1a6..444e11d0d563d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx @@ -18,15 +18,8 @@ */ import React, { ChangeEvent } from 'react'; import { get } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - htmlIdGenerator, - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiSpacer, -} from '@elastic/eui'; + +import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { AddDeleteButtons } from '../../add_delete_buttons'; @@ -50,8 +43,6 @@ export const MultiValueRow = ({ disableAdd, disableDelete, }: MultiValueRowProps) => { - const htmlId = htmlIdGenerator(); - const onFieldNumberChange = (event: ChangeEvent) => onChange({ ...model, @@ -59,17 +50,9 @@ export const MultiValueRow = ({ }); return ( -
- - - - - - - + + + onDelete(model)} disableDelete={disableDelete} disableAdd={disableAdd} + responsive={false} /> - -
+ ); }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index d02a16ade2bba..f78df9b1ddef4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { htmlIdGenerator, - EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiFormRow, EuiSpacer, + EuiFlexGrid, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AggSelect } from '../agg_select'; @@ -34,12 +34,16 @@ import { FieldSelect } from '../field_select'; import { createChangeHandler } from '../../lib/create_change_handler'; // @ts-ignore import { createSelectHandler } from '../../lib/create_select_handler'; +// @ts-ignore +import { createNumberHandler } from '../../lib/create_number_handler'; + import { AggRow } from '../agg_row'; import { PercentileRankValues } from './percentile_rank_values'; import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types'; import { DragHandleProps } from '../../../../types'; +import { PercentileHdr } from '../percentile_hdr'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; @@ -67,6 +71,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { const isTablePanel = panel.type === 'table'; const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); const handlePercentileRankValuesChange = (values: MetricsItemsSchema['values']) => { handleChange({ @@ -84,7 +89,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { siblings={props.siblings} dragHandleProps={props.dragHandleProps} > - + { /> - - - {model.values && ( - - )} + + + + } + > + + + + + + + ); }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx index b66d79d67f427..92ca4dfb706ac 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { last } from 'lodash'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { MultiValueRow } from './multi_value_row'; interface PercentileRankValuesProps { @@ -52,19 +52,20 @@ export const PercentileRankValues = (props: PercentileRankValuesProps) => { disableDeleteRow: boolean; disableAddRow: boolean; }) => ( - + + + ); return ( - + {showOnlyLastRow && renderRow({ rowModel: { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js index bd421248a3607..fb556a053df3f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js @@ -19,6 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import { collectionActions } from '../lib/collection_actions'; import { AddDeleteButtons } from '../add_delete_buttons'; @@ -27,17 +28,19 @@ import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, - EuiFormLabel, EuiComboBox, EuiFieldNumber, + EuiFormRow, + EuiFlexGrid, + EuiPanel, } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; export const newPercentile = (opts) => { return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts); }; -class PercentilesUi extends Component { +export class Percentiles extends Component { handleTextChange(item, name) { return (e) => { const handleChange = collectionActions.handleChange.bind(null, this.props); @@ -50,22 +53,31 @@ class PercentilesUi extends Component { renderRow = (row, i, items) => { const defaults = { value: '', percentile: '', shade: '' }; const model = { ...defaults, ...row }; - const { intl, panel } = this.props; + const { panel } = this.props; + const flexItemStyle = { minWidth: 100 }; const percentileFieldNumber = ( - - + + + } + > + + ); @@ -77,99 +89,103 @@ class PercentilesUi extends Component { const handleDelete = collectionActions.handleDelete.bind(null, this.props, model); const modeOptions = [ { - label: intl.formatMessage({ - id: 'visTypeTimeseries.percentile.modeOptions.lineLabel', + label: i18n.translate('visTypeTimeseries.percentile.modeOptions.lineLabel', { defaultMessage: 'Line', }), value: 'line', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.percentile.modeOptions.bandLabel', + label: i18n.translate('visTypeTimeseries.percentile.modeOptions.bandLabel', { defaultMessage: 'Band', }), value: 'band', }, ]; - const optionsStyle = {}; + const optionsStyle = { + ...flexItemStyle, + }; if (model.mode === 'line') { optionsStyle.display = 'none'; } - const labelStyle = { marginBottom: 0 }; + const htmlId = htmlIdGenerator(model.id); const selectedModeOption = modeOptions.find((option) => { return model.mode === option.value; }); return ( - - {percentileFieldNumber} - - - - - - - - - - - - - + + + + {percentileFieldNumber} + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + + - - - - - - - - - - - - - - - - - - - + + + ); }; @@ -192,15 +208,13 @@ class PercentilesUi extends Component { } } -PercentilesUi.defaultProps = { +Percentiles.defaultProps = { name: 'percentile', }; -PercentilesUi.propTypes = { +Percentiles.propTypes = { name: PropTypes.string, model: PropTypes.object, panel: PropTypes.object, onChange: PropTypes.func, }; - -export const Percentiles = injectI18n(PercentilesUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 34f339ce24c21..0f64c570088d7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -21,6 +21,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import { createTickFormatter } from './tick_formatter'; +import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => { @@ -63,15 +64,7 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig * If not, return a formatted value from elasticsearch */ if (row.labelFormatted) { - const momemntObj = moment(row.labelFormatted); - let val; - - if (momemntObj.isValid()) { - val = momemntObj.format(dateFormat); - } else { - val = row.labelFormatted; - } - + const val = labelDateFormatter(row.labelFormatted, dateFormat); set(variables, `${_.snakeCase(row.label)}.formatted`, val); } }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts new file mode 100644 index 0000000000000..c4a0f10c5748c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { labelDateFormatter } from './label_date_formatter'; + +const dateString = '2020-09-24T18:59:02.000Z'; + +describe('Label Date Formatter Function', () => { + it('Should format the date string', () => { + const label = labelDateFormatter(dateString); + expect(label).toEqual(moment(dateString).format('lll')); + }); + + it('Should format the date string on the given formatter', () => { + const label = labelDateFormatter(dateString, 'MM/DD/YYYY'); + expect(label).toEqual(moment(dateString).format('MM/DD/YYYY')); + }); + + it('Returns the label if it is not date string', () => { + const label = labelDateFormatter('test date'); + expect(label).toEqual('test date'); + }); + + it('Returns the label if it is a number string', () => { + const label = labelDateFormatter('1'); + expect(label).toEqual('1'); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts new file mode 100644 index 0000000000000..f4de19b084c7c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.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 moment from 'moment'; + +export const labelDateFormatter = (label: string, dateformat = 'lll') => { + let formattedLabel = label; + // Use moment isValid function on strict mode + const isDate = moment(label, '', true).isValid(); + if (isDate) { + formattedLabel = moment(label).format(dateformat); + } + return formattedLabel; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index c3e18ec202c0f..a219d7e8ee174 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -36,8 +36,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getDefaultQueryLanguage } from './lib/get_default_query_language'; -import { QueryBarWrapper } from './query_bar_wrapper'; +import { SeriesConfigQueryBarWithIgnoreGlobalFilter } from './series_config_query_bar_with_ignore_global_filter'; export const SeriesConfig = (props) => { const defaults = { offset_time: '', value_template: '' }; @@ -56,28 +55,12 @@ export const SeriesConfig = (props) => { - - } - fullWidth - > - props.onChange({ filter })} - indexPatterns={[seriesIndexPattern]} - /> - + @@ -162,6 +145,7 @@ export const SeriesConfig = (props) => { SeriesConfig.propTypes = { fields: PropTypes.object, + panel: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, indexPatternForQuery: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js new file mode 100644 index 0000000000000..b1275335522e6 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 PropTypes from 'prop-types'; +import React from 'react'; +import { htmlIdGenerator, EuiFlexItem, EuiFormRow, EuiToolTip, EuiFlexGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { YesNo } from './yes_no'; +import { getDefaultQueryLanguage } from './lib/get_default_query_language'; +import { QueryBarWrapper } from './query_bar_wrapper'; + +export function SeriesConfigQueryBarWithIgnoreGlobalFilter({ + panel, + model, + onChange, + indexPatternForQuery, +}) { + const htmlId = htmlIdGenerator(); + const yesNoOption = ( + + ); + return ( + + + + } + fullWidth + > + onChange({ filter })} + indexPatterns={[indexPatternForQuery]} + /> + + + + + } + fullWidth + > + {panel.ignore_global_filter ? ( + + } + > + {yesNoOption} + + ) : ( + yesNoOption + )} + + + + ); +} + +SeriesConfigQueryBarWithIgnoreGlobalFilter.propTypes = { + onChange: PropTypes.func, + model: PropTypes.object, + panel: PropTypes.object, + indexPatternForQuery: PropTypes.string, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js index 851e900b5a622..7550f48386378 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js @@ -90,6 +90,7 @@ function GaugeSeriesUi(props) { seriesBody = ( - - - } - fullWidth - > - props.onChange({ filter })} - indexPatterns={[seriesIndexPattern]} - /> - - + {type} @@ -584,6 +565,7 @@ export const TimeseriesConfig = injectI18n(function (props) { TimeseriesConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, + panel: PropTypes.object, onChange: PropTypes.func, indexPatternForQuery: PropTypes.string, seriesQuantity: PropTypes.object, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js index 06ad7e14bf358..680c1c5e78ad4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js @@ -97,6 +97,7 @@ const TimeseriesSeriesUI = injectI18n(function (props) { seriesBody = ( { const splitData = splitsVisData[key]; - const { series, label } = splitData; + const { series, label, labelFormatted } = splitData; + let additionalLabel = label; + if (labelFormatted) { + additionalLabel = labelDateFormatter(labelFormatted); + } const newSeries = indexOfNonSplit != null && indexOfNonSplit > 0 ? [...series, nonSplitSeries] @@ -84,7 +90,7 @@ export function visWithSplits(WrappedComponent) { model={model} visData={newVisData} onBrush={props.onBrush} - additionalLabel={label} + additionalLabel={additionalLabel} backgroundColor={props.backgroundColor} getConfig={props.getConfig} /> diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 1209a105af805..278d7906dde94 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -20,6 +20,7 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { labelDateFormatter } from '../../../components/lib/label_date_formatter'; import { Axis, @@ -70,19 +71,20 @@ export const TimeSeries = ({ annotations, }) => { const chartRef = useRef(); - const updateCursor = (_, cursor) => { - if (chartRef.current) { - chartRef.current.dispatchExternalPointerEvent(cursor); - } - }; useEffect(() => { + const updateCursor = (_, cursor) => { + if (chartRef.current) { + chartRef.current.dispatchExternalPointerEvent(cursor); + } + }; + eventBus.on(ACTIVE_CURSOR, updateCursor); return () => { eventBus.off(ACTIVE_CURSOR, undefined, updateCursor); }; - }, []); // eslint-disable-line + }, []); const tooltipFormatter = decorateFormatter(xAxisFormatter); const uiSettings = getUISettings(); @@ -139,6 +141,7 @@ export const TimeSeries = ({ type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, headerFormatter: tooltipFormatter, }} + externalPointerEvents={{ tooltip: { visible: false } }} /> {annotations.map(({ id, data, icon, color }) => { @@ -163,6 +166,7 @@ export const TimeSeries = ({ { id, label, + labelFormatted, bars, lines, data, @@ -186,14 +190,17 @@ export const TimeSeries = ({ const key = `${id}-${label}`; // Only use color mapping if there is no color from the server const finalColor = color ?? colors.mappedColors.mapping[label]; - + let seriesName = label.toString(); + if (labelFormatted) { + seriesName = labelDateFormatter(labelFormatted); + } if (bars?.show) { return ( - {item.label} + {item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label}
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 26a1792e3ec70..682d0a071e50d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -62,8 +62,11 @@ export async function getFields( let indexPatternString = indexPattern; if (!indexPatternString) { - const [, { data }] = await framework.core.getStartServices(); - const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory(request); + const [{ savedObjects }, { data }] = await framework.core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(request); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient + ); const defaultIndexPattern = await indexPatternsService.getDefault(); indexPatternString = get(defaultIndexPattern, 'title', ''); } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 4dcc67dc46976..ceae784cf74a6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import { AbstractSearchStrategy } from './abstract_search_strategy'; describe('AbstractSearchStrategy', () => { @@ -55,7 +56,7 @@ describe('AbstractSearchStrategy', () => { test('should return response', async () => { const searches = [{ body: 'body', index: 'index' }]; - const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); + const searchFn = jest.fn().mockReturnValue(from(Promise.resolve({}))); const responses = await abstractSearchStrategy.search( { @@ -82,7 +83,6 @@ describe('AbstractSearchStrategy', () => { expect(responses).toEqual([{}]); expect(searchFn).toHaveBeenCalledWith( - {}, { params: { body: 'body', @@ -92,7 +92,8 @@ describe('AbstractSearchStrategy', () => { }, { strategy: 'es', - } + }, + {} ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 2eb92b2b777e8..7b62ad310a354 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -60,20 +60,22 @@ export class AbstractSearchStrategy { const requests: any[] = []; bodies.forEach((body) => { requests.push( - deps.data.search.search( - req.requestContext, - { - params: { - ...body, - ...this.additionalParams, + deps.data.search + .search( + { + params: { + ...body, + ...this.additionalParams, + }, + indexType: this.indexType, }, - indexType: this.indexType, - }, - { - ...options, - strategy: this.searchStrategyName, - } - ) + { + ...options, + strategy: this.searchStrategyName, + }, + req.requestContext + ) + .toPromise() ); }); return Promise.all(requests); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index 1eace13c2e336..232efc7514a5a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -43,7 +43,6 @@ export async function getSeriesData(req, panel) { (acc, items) => acc.concat(items), [] ); - const data = await searchStrategy.search(req, searches); const handleResponseBodyFn = handleResponseBody(panel); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index f033a43806312..dc2936072165e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -64,6 +64,16 @@ function extendStatsBucket(bucket, metrics) { return body; } +function getPercentileHdrParam(bucket) { + return bucket.numberOfSignificantValueDigits + ? { + hdr: { + number_of_significant_value_digits: bucket.numberOfSignificantValueDigits, + }, + } + : undefined; +} + export const bucketTransform = { count: () => { return { @@ -139,13 +149,14 @@ export const bucketTransform = { bucket.percentiles.filter((p) => p.percentile).map((p) => p.percentile) ); } - const agg = { + + return { percentiles: { field: bucket.field, percents, + ...getPercentileHdrParam(bucket), }, }; - return agg; }, percentile_rank: (bucket) => { @@ -155,6 +166,7 @@ export const bucketTransform = { percentile_ranks: { field: bucket.field, values: (bucket.values || []).map((value) => (isEmpty(value) ? 0 : value)), + ...getPercentileHdrParam(bucket), }, }; }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js index 906b3e71c7cb9..e0a4f90b1bb35 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js @@ -27,8 +27,9 @@ export function query(req, panel, series, esQueryConfig, indexPatternObject) { const { from, to } = offsetTime(req, series.offset_time); doc.size = 0; - const queries = !panel.ignore_global_filter ? req.payload.query : []; - const filters = !panel.ignore_global_filter ? req.payload.filters : []; + const ignoreGlobalFilter = panel.ignore_global_filter || series.ignore_global_filter; + const queries = !ignoreGlobalFilter ? req.payload.query : []; + const filters = !ignoreGlobalFilter ? req.payload.filters : []; doc.query = esQuery.buildEsQuery(indexPatternObject, queries, filters, esQueryConfig); const timerange = { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js index daf89b08fab4b..feb81d98f6246 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js @@ -318,4 +318,60 @@ describe('query(req, panel, series)', () => { }, }); }); + + test('returns doc with panel filter (ignoring globals from series)', () => { + req.payload.filters = [ + { + bool: { + must: [ + { + term: { + host: 'example', + }, + }, + ], + }, + }, + ]; + panel.filter = { query: 'host:web-server', language: 'lucene' }; + series.ignore_global_filter = true; + const next = (doc) => doc; + const doc = query(req, panel, series, config)(next)({}); + expect(doc).toEqual({ + size: 0, + query: { + bool: { + filter: [], + must: [ + { + range: { + timestamp: { + gte: '2017-01-01T00:00:00.000Z', + lte: '2017-01-01T01:00:00.000Z', + format: 'strict_date_optional_time', + }, + }, + }, + { + bool: { + filter: [], + must: [ + { + query_string: { + query: panel.filter.query, + analyze_wildcard: true, + }, + }, + ], + must_not: [], + should: [], + }, + }, + ], + must_not: [], + should: [], + }, + }, + }); + }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js index a11324f73e611..c163605af7ac5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js @@ -32,7 +32,7 @@ export function percentileRank(resp, panel, series, meta) { } getSplits(resp, panel, series, meta).forEach((split) => { - (metric.values || []).forEach((percentileRank) => { + (metric.values || []).forEach((percentileRank, index) => { const data = split.timeseries.buckets.map((bucket) => [ bucket.key, getAggValue(bucket, { @@ -43,7 +43,7 @@ export function percentileRank(resp, panel, series, meta) { results.push({ data, - id: `${split.id}:${percentileRank}`, + id: `${split.id}:${percentileRank}:${index}`, label: `${split.label} (${percentileRank || 0})`, color: split.color, ...getDefaultDecoration(series), diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index a5f095a4c4f3d..0969174c7143c 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { tsvbTelemetrySavedObjectType } from '../saved_objects'; @@ -49,7 +49,7 @@ export class ValidationTelemetryService implements Plugin({ type: 'tsvb-validation', isReady: () => this.kibanaIndex !== '', - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async ({ callCluster }) => { try { const response = await callCluster('get', { index: this.kibanaIndex, diff --git a/src/plugins/vis_type_timelion/public/flot/index.js b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts similarity index 78% rename from src/plugins/vis_type_timelion/public/flot/index.js rename to src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts index a066fd3ab8607..70be4c273b77f 100644 --- a/src/plugins/vis_type_timelion/public/flot/index.js +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts @@ -17,10 +17,8 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +export const mockStats = { somestat: 1 }; +export const mockGetStats = jest.fn().mockResolvedValue(mockStats); +jest.doMock('./get_usage_collector', () => ({ + getStats: mockGetStats, +})); diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index f2854896791c1..6f17703bc9dee 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -17,11 +17,9 @@ * under the License. */ -import { of } from 'rxjs'; - -import { LegacyAPICaller } from 'src/core/server'; -import { getUsageCollector } from './get_usage_collector'; +import { getStats } from './get_usage_collector'; import { HomeServerPluginSetup } from '../../../home/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; const mockedSavedObjects = [ // vega-lite lib spec @@ -72,12 +70,15 @@ const mockedSavedObjects = [ }, ]; -const getMockCallCluster = (hits?: unknown[]) => - jest.fn().mockReturnValue(Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; +const getMockCollectorFetchContext = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; describe('Vega visualization usage collector', () => { - const configMock = of({ kibana: { index: '' } }); - const usageCollector = getUsageCollector(configMock, { + const mockIndex = 'mock_index'; + const mockDeps = { home: ({ sampleData: { getSampleDatasets: jest.fn().mockReturnValue([ @@ -100,44 +101,41 @@ describe('Vega visualization usage collector', () => { ]), }, } as unknown) as HomeServerPluginSetup, - }); - - test('Should fit the shape', () => { - expect(usageCollector.type).toBe('vis_type_vega'); - expect(usageCollector.isReady()).toBe(true); - expect(usageCollector.fetch).toEqual(expect.any(Function)); - }); + }; test('Returns undefined when no results found (undefined)', async () => { - const result = await usageCollector.fetch(getMockCallCluster()); + const result = await getStats(getMockCollectorFetchContext().callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await usageCollector.fetch(getMockCallCluster([])); + const result = await getStats( + getMockCollectorFetchContext([]).callCluster, + mockIndex, + mockDeps + ); expect(result).toBeUndefined(); }); test('Returns undefined when no vega saved objects found', async () => { - const result = await usageCollector.fetch( - getMockCallCluster([ - { - _id: 'visualization:myvis-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "area"}' }, - }, + const mockCollectorFetchContext = getMockCollectorFetchContext([ + { + _id: 'visualization:myvis-123', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "area"}' }, }, - ]) - ); + }, + ]); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Should ingnore sample data visualizations', async () => { - const callCluster = getMockCallCluster([ + const mockCollectorFetchContext = getMockCollectorFetchContext([ { _id: 'visualization:sampledata-123', _source: { @@ -155,13 +153,14 @@ describe('Vega visualization usage collector', () => { }, ]); - const result = await usageCollector.fetch(callCluster); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { - const result = await usageCollector.fetch(getMockCallCluster(mockedSavedObjects)); + const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toMatchObject({ vega_lib_specs_total: 2, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index d5e691ca48301..b0de8eb2f5140 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -17,23 +17,16 @@ * under the License. */ import { parse } from 'hjson'; -import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { LegacyAPICaller, SavedObject } from 'src/core/server'; -import { - ConfigObservable, - VegaSavedObjectAttributes, - VisTypeVegaPluginSetupDependencies, -} from '../types'; +import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; type UsageCollectorDependencies = Pick; type ESResponse = SearchResponse<{ visualization: { visState: string } }>; type VegaType = 'vega' | 'vega-lite'; -const VEGA_USAGE_TYPE = 'vis_type_vega'; - function isVegaType(attributes: any): attributes is VegaSavedObjectAttributes { return attributes && attributes.type === 'vega' && attributes.params?.spec; } @@ -64,11 +57,25 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home']) return titles; }; -const getStats = async ( +export interface VegaUsage { + vega_lib_specs_total: number; + vega_lite_lib_specs_total: number; + vega_use_map_total: number; +} + +export const getStats = async ( callCluster: LegacyAPICaller, index: string, { home }: UsageCollectorDependencies -) => { +): Promise => { + let shouldPublishTelemetry = false; + + const vegaUsage = { + vega_lib_specs_total: 0, + vega_lite_lib_specs_total: 0, + vega_use_map_total: 0, + }; + const searchParams = { size: 10000, index, @@ -82,9 +89,9 @@ const getStats = async ( }, }, }; + const esResponse: ESResponse = await callCluster('search', searchParams); const size = esResponse?.hits?.hits?.length ?? 0; - let shouldPublishTelemetry = false; if (!size) { return; @@ -93,55 +100,32 @@ const getStats = async ( // we want to exclude the Vega Sample Data visualizations from the stats // in order to have more accurate results const excludedFromStatsVisualizations = getDefaultVegaVisualizations(home); + for (const hit of esResponse.hits.hits) { + const visualization = hit._source?.visualization; + const visState = JSON.parse(visualization?.visState ?? '{}'); + + if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) { + try { + const spec = parse(visState.params.spec, { legacyRoot: false }); + + if (spec) { + shouldPublishTelemetry = true; - const finalTelemetry = esResponse.hits.hits.reduce( - (telemetry, hit) => { - const visualization = hit._source?.visualization; - const visState = JSON.parse(visualization?.visState ?? '{}'); - - if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) - try { - const spec = parse(visState.params.spec, { legacyRoot: false }); - - if (spec) { - shouldPublishTelemetry = true; - if (checkVegaSchemaType(spec.$schema, 'vega')) { - telemetry.vega_lib_specs_total++; - } - if (checkVegaSchemaType(spec.$schema, 'vega-lite')) { - telemetry.vega_lite_lib_specs_total++; - } - if (spec.config?.kibana?.type === 'map') { - telemetry.vega_use_map_total++; - } + if (checkVegaSchemaType(spec.$schema, 'vega')) { + vegaUsage.vega_lib_specs_total++; + } + if (checkVegaSchemaType(spec.$schema, 'vega-lite')) { + vegaUsage.vega_lite_lib_specs_total++; + } + if (spec.config?.kibana?.type === 'map') { + vegaUsage.vega_use_map_total++; } - } catch (e) { - // Let it go, the data is invalid and we'll don't need to handle it } - - return telemetry; - }, - { - vega_lib_specs_total: 0, - vega_lite_lib_specs_total: 0, - vega_use_map_total: 0, + } catch (e) { + // Let it go, the data is invalid and we'll don't need to handle it + } } - ); + } - return shouldPublishTelemetry ? finalTelemetry : undefined; + return shouldPublishTelemetry ? vegaUsage : undefined; }; - -export function getUsageCollector( - config: ConfigObservable, - dependencies: UsageCollectorDependencies -) { - return { - type: VEGA_USAGE_TYPE, - isReady: () => true, - fetch: async (callCluster: LegacyAPICaller) => { - const { index } = (await config.pipe(first()).toPromise()).kibana; - - return await getStats(callCluster, index, dependencies); - }, - }; -} diff --git a/src/plugins/vis_type_vega/server/usage_collector/index.ts b/src/plugins/vis_type_vega/server/usage_collector/index.ts index 4fc12a14054ec..decc3ef1ff1d9 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/index.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/index.ts @@ -17,16 +17,4 @@ * under the License. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { getUsageCollector } from './get_usage_collector'; -import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types'; - -export function registerVegaUsageCollector( - collectorSet: UsageCollectionSetup, - config: ConfigObservable, - dependencies: Pick -) { - const collector = collectorSet.makeUsageCollector(getUsageCollector(config, dependencies)); - - collectorSet.registerCollector(collector); -} +export { registerVegaUsageCollector } from './register_vega_collector'; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts new file mode 100644 index 0000000000000..e092fc8acfd71 --- /dev/null +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { of } from 'rxjs'; +import { mockStats, mockGetStats } from './get_usage_collector.mock'; +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { HomeServerPluginSetup } from '../../../home/server'; +import { registerVegaUsageCollector } from './register_vega_collector'; + +describe('registerVegaUsageCollector', () => { + const mockIndex = 'mock_index'; + const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; + const mockConfig = of({ kibana: { index: mockIndex } }); + + it('makes a usage collector and registers it`', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + }); + + it('makeUsageCollector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'vis_type_vega', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: expect.any(Object), + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.isReady returns true', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + const mockedCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockedCollectorFetchContext); + expect(mockGetStats).toBeCalledTimes(1); + expect(mockGetStats).toBeCalledWith( + mockedCollectorFetchContext.callCluster, + mockIndex, + mockDeps + ); + expect(fetchResult).toBe(mockStats); + }); +}); diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts new file mode 100644 index 0000000000000..e4772dad99d40 --- /dev/null +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { first } from 'rxjs/operators'; +import { getStats, VegaUsage } from './get_usage_collector'; +import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types'; + +export function registerVegaUsageCollector( + collectorSet: UsageCollectionSetup, + config: ConfigObservable, + dependencies: Pick +) { + const collector = collectorSet.makeUsageCollector({ + type: 'vis_type_vega', + isReady: () => true, + schema: { + vega_lib_specs_total: { type: 'long' }, + vega_lite_lib_specs_total: { type: 'long' }, + vega_use_map_total: { type: 'long' }, + }, + fetch: async ({ callCluster }) => { + const { index } = (await config.pipe(first()).toPromise()).kibana; + + return await getStats(callCluster, index, dependencies); + }, + }); + + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index bf36bb35d0563..0ced74e2733d3 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,7 +3,14 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector", "dashboard"], + "requiredPlugins": [ + "data", + "expressions", + "uiActions", + "embeddable", + "inspector", + "savedObjects" + ], "optionalPlugins": ["usageCollection"], - "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] + "requiredBundles": ["kibanaUtils", "discover"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index b27d24d980e8d..36f3f7d6ed22e 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -25,7 +25,11 @@ import { VisualizeByReferenceInput, VisualizeSavedObjectAttributes, } from './visualize_embeddable'; -import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public'; +import { + IContainer, + ErrorEmbeddable, + AttributeService, +} from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { getSavedVisualizationsLoader, @@ -37,7 +41,6 @@ import { import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; import { SavedVisualizationsLoader } from '../saved_visualizations'; -import { AttributeService } from '../../../dashboard/public'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index fe8a9adff4052..a810b4b65528f 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -38,6 +38,7 @@ import { Adapters, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, + AttributeService, } from '../../../../plugins/embeddable/public'; import { IExpressionLoaderParams, @@ -51,7 +52,6 @@ import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { TriggerId } from '../../../ui_actions/public'; import { SavedObjectAttributes } from '../../../../core/types'; -import { AttributeService } from '../../../dashboard/public'; import { SavedVisualizationsLoader } from '../saved_visualizations'; import { VisSavedObject } from '../types'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 87f78f5639ff0..4b851da6be70e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -26,6 +26,7 @@ import { EmbeddableOutput, ErrorEmbeddable, IContainer, + AttributeService, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { @@ -50,7 +51,6 @@ import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_obje import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; -import { AttributeService } from '../../../dashboard/public'; import { checkForDuplicateTitle } from '../../../saved_objects/public'; interface VisualizationAttributes extends SavedObjectAttributes { @@ -126,7 +126,7 @@ export class VisualizeEmbeddableFactory if (!this.attributeService) { this.attributeService = await this.deps .start() - .plugins.dashboard.getAttributeService< + .plugins.embeddable.getAttributeService< VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 99c13b42b8b28..081399fd1fbea 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -36,7 +36,7 @@ export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; -export { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; +export type { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index c0c37e2262f9c..cbdecd4aac747 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -12,16 +12,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index a1fea45f51781..c744043ed155b 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -117,84 +117,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - describe('handles table function', () => { - it('without splits or buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - split_row: [1, 2], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits and buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - split_row: [2, 4], - bucket: [3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { - const params = { - showMetricsAtAllLevels: true, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { - const params = { - showMetricsAtAllLevels: false, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 9f6a4d5553292..eb431212166a3 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -39,14 +39,14 @@ export interface SchemaConfig { export interface Schemas { metric: SchemaConfig[]; - bucket?: any[]; + bucket?: SchemaConfig[]; geo_centroid?: any[]; group?: any[]; params?: any[]; radius?: any[]; segment?: any[]; - split_column?: any[]; - split_row?: any[]; + split_column?: SchemaConfig[]; + split_row?: SchemaConfig[]; width?: any[]; // catch all for schema name [key: string]: any[] | undefined; @@ -267,13 +267,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); return `tsvb ${paramsArray.join(' ')}`; }, - table: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.table(schemas, params), - }; - return `kibana_table ${prepareJson('visConfig', visConfig)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, @@ -298,26 +291,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; const buildVisConfig: BuildVisConfigFunction = { - table: (schemas, visParams = {}) => { - const visConfig = {} as any; - const metrics = schemas.metric; - const buckets = schemas.bucket || []; - visConfig.dimensions = { - metrics, - buckets, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - - if (visParams.showMetricsAtAllLevels === false && visParams.showPartialRows === true) { - // Handle case where user wants to see partial rows but not metrics at all levels. - // This requires calculating how many metrics will come back in the tabified response, - // and removing all metrics from the dimensions except the last set. - const metricsPerBucket = metrics.length / buckets.length; - visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); - } - return visConfig; - }, region_map: (schemas) => { const visConfig = {} as any; visConfig.metric = schemas.metric[0]; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 90e4936a58b45..f20e87dbd3b6a 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -28,6 +28,7 @@ import { usageCollectionPluginMock } from '../../../plugins/usage_collection/pub import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; +import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -73,6 +74,7 @@ const createInstance = async () => { dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, + savedObjects: savedObjectsPluginMock.createStartContract(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 37a9972983421..c1dbe39def64c 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -78,6 +78,7 @@ import { } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; import { DashboardStart } from '../../dashboard/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -112,7 +113,8 @@ export interface VisualizationsStartDeps { uiActions: UiActionsStart; application: ApplicationStart; dashboard: DashboardStart; - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; + savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; } @@ -160,7 +162,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard, savedObjects }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); @@ -182,18 +184,13 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, visualizationTypes: types, }); setSavedVisualizationsLoader(savedVisualizationsLoader); const savedSearchLoader = createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, }); setSavedSearchLoader(savedSearchLoader); return { diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index 8edf494ddc0ec..59359fb00cc9f 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -24,17 +24,20 @@ * * NOTE: It's a type of SavedObject, but specific to visualizations. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern } from '../../../../plugins/data/public'; +import { IIndexPattern, IndexPatternsContract } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../discover/public'; +import { SavedObjectsClientContract } from '../../../../core/public'; + +export interface SavedVisServices { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; + indexPatterns: IndexPatternsContract; +} export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; @@ -73,11 +76,10 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; }; -export function createSavedVisClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); +export function createSavedVisClass(services: SavedVisServices) { const savedSearch = createSavedSearchesLoader(services); - class SavedVis extends SavedObjectClass { + class SavedVis extends services.savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { title: 'text', @@ -130,5 +132,5 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) { } } - return SavedVis as new (opts: Record | string) => SavedObject; + return (SavedVis as unknown) as new (opts: Record | string) => SavedObject; } diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts index 0ec3c0dab2e97..760bf3cc7a362 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts @@ -16,19 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { - SavedObjectLoader, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { findListItems } from './find_list_items'; -import { createSavedVisClass } from './_saved_vis'; +import { createSavedVisClass, SavedVisServices } from './_saved_vis'; import { TypesStart } from '../vis_types'; -export interface SavedObjectKibanaServicesWithVisualizations extends SavedObjectKibanaServices { +export interface SavedVisServicesWithVisualizations extends SavedVisServices { visualizationTypes: TypesStart; } export type SavedVisualizationsLoader = ReturnType; -export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisualizations) { +export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; class SavedObjectLoaderVisualize extends SavedObjectLoader { diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 22561decabea4..a46b257c9905c 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,6 +18,6 @@ */ export * from './types_service'; -export { VisType } from './types'; +export type { VisType } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 0cf345bf07be6..7206e9612f102 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -20,6 +20,7 @@ import { IconType } from '@elastic/eui'; import React from 'react'; import { Adapters } from 'src/plugins/inspector'; +import { VisEditorConstructor } from 'src/plugins/visualize/public'; import { ISchemas } from 'src/plugins/vis_default_editor/public'; import { TriggerContextMapping } from '../../../ui_actions/public'; import { Vis, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; @@ -69,12 +70,14 @@ export interface VisType { readonly options: VisTypeOptions; - // TODO: The following types still need to be refined properly. - /** * The editor that should be used to edit visualizations of this type. + * If this is not specified the default visualize editor will be used (and should be configured via schemas) + * and editorConfig. */ - readonly editor?: any; + readonly editor?: VisEditorConstructor; + + // TODO: The following types still need to be refined properly. readonly editorConfig: Record; readonly visConfig: Record; } diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index ad7618a8640ba..5261b2cac7dcf 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -45,13 +45,13 @@ export const visualizationSavedObjectType: SavedObjectsType = { properties: { description: { type: 'text' }, kibanaSavedObjectMeta: { - properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + properties: { searchSourceJSON: { type: 'text', index: false } }, }, savedSearchRefName: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, - uiStateJSON: { type: 'text', index: false, doc_values: false }, + uiStateJSON: { type: 'text', index: false }, version: { type: 'integer' }, - visState: { type: 'text', index: false, doc_values: false }, + visState: { type: 'text', index: false }, }, }, migrations: visualizationSavedObjectTypeMigrations, diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.mock.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.mock.ts new file mode 100644 index 0000000000000..70be4c273b77f --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.mock.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 const mockStats = { somestat: 1 }; +export const mockGetStats = jest.fn().mockResolvedValue(mockStats); +jest.doMock('./get_usage_collector', () => ({ + getStats: mockGetStats, +})); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts index 108f270bc8eab..fd363b3505186 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts @@ -18,10 +18,8 @@ */ import moment from 'moment'; -import { of } from 'rxjs'; - import { LegacyAPICaller } from 'src/core/server'; -import { getUsageCollector } from './get_usage_collector'; +import { getStats } from './get_usage_collector'; const defaultMockSavedObjects = [ { @@ -121,29 +119,22 @@ const enlargedMockSavedObjects = [ ]; describe('Visualizations usage collector', () => { - const configMock = of({ kibana: { index: '' } }); - const usageCollector = getUsageCollector(configMock); + const mockIndex = ''; const getMockCallCluster = (hits: unknown[]) => (() => Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; - test('Should fit the shape', () => { - expect(usageCollector.type).toBe('visualization_types'); - expect(usageCollector.isReady()).toBe(true); - expect(usageCollector.fetch).toEqual(expect.any(Function)); - }); - test('Returns undefined when no results found (undefined)', async () => { - const result = await usageCollector.fetch(getMockCallCluster(undefined as any)); - expect(result).toBe(undefined); + const result = await getStats(getMockCallCluster(undefined as any), mockIndex); + expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await usageCollector.fetch(getMockCallCluster([])); - expect(result).toBe(undefined); + const result = await getStats(getMockCallCluster([]), mockIndex); + expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { - const result = await usageCollector.fetch(getMockCallCluster(defaultMockSavedObjects)); + const result = await getStats(getMockCallCluster(defaultMockSavedObjects), mockIndex); expect(result).toMatchObject({ shell_beads: { @@ -198,7 +189,7 @@ describe('Visualizations usage collector', () => { }, }; - const result = await usageCollector.fetch(getMockCallCluster(enlargedMockSavedObjects)); + const result = await getStats(getMockCallCluster(enlargedMockSavedObjects), mockIndex); expect(result).toMatchObject(expectedStats); }); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts index 9ba09a3303b5f..aed9a54dcf01a 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts @@ -17,16 +17,12 @@ * under the License. */ -import { Observable } from 'rxjs'; import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash'; -import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { LegacyAPICaller } from 'src/core/server'; import { getPastDays } from './get_past_days'; -const VIS_USAGE_TYPE = 'visualization_types'; - type ESResponse = SearchResponse<{ visualization: { visState: string } }>; interface VisSummary { @@ -35,10 +31,25 @@ interface VisSummary { past_days: number; } +export interface VisualizationUsage { + [x: string]: { + total: number; + spaces_min?: number; + spaces_max?: number; + spaces_avg: number; + saved_7_days_total: number; + saved_30_days_total: number; + saved_90_days_total: number; + }; +} + /* * Parse the response data into telemetry payload */ -async function getStats(callCluster: LegacyAPICaller, index: string) { +export async function getStats( + callCluster: LegacyAPICaller, + index: string +): Promise { const searchParams = { size: 10000, // elasticsearch index.max_result_window default value index, @@ -94,14 +105,3 @@ async function getStats(callCluster: LegacyAPICaller, index: string) { }; }); } - -export function getUsageCollector(config: Observable<{ kibana: { index: string } }>) { - return { - type: VIS_USAGE_TYPE, - isReady: () => true, - fetch: async (callCluster: LegacyAPICaller) => { - const index = (await config.pipe(first()).toPromise()).kibana.index; - return await getStats(callCluster, index); - }, - }; -} diff --git a/src/plugins/visualizations/server/usage_collector/index.ts b/src/plugins/visualizations/server/usage_collector/index.ts index ed890a6b5e48a..fb7e895d9d64a 100644 --- a/src/plugins/visualizations/server/usage_collector/index.ts +++ b/src/plugins/visualizations/server/usage_collector/index.ts @@ -17,15 +17,4 @@ * under the License. */ -import { Observable } from 'rxjs'; - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { getUsageCollector } from './get_usage_collector'; - -export function registerVisualizationsCollector( - collectorSet: UsageCollectionSetup, - config: Observable<{ kibana: { index: string } }> -) { - const collector = collectorSet.makeUsageCollector(getUsageCollector(config)); - collectorSet.registerCollector(collector); -} +export { registerVisualizationsCollector } from './register_visualizations_collector'; diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts new file mode 100644 index 0000000000000..7789e3de13e5a --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.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 { of } from 'rxjs'; +import { mockStats, mockGetStats } from './get_usage_collector.mock'; +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; + +import { registerVisualizationsCollector } from './register_visualizations_collector'; + +describe('registerVisualizationsCollector', () => { + const mockIndex = 'mock_index'; + const mockConfig = of({ kibana: { index: mockIndex } }); + + it('makes a usage collector and registers it`', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisualizationsCollector(mockCollectorSet, mockConfig); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + }); + + it('makeUsageCollector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisualizationsCollector(mockCollectorSet, mockConfig); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'visualization_types', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: expect.any(Object), + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.isReady returns true', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisualizationsCollector(mockCollectorSet, mockConfig); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('makeUsageCollector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisualizationsCollector(mockCollectorSet, mockConfig); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockCollectorFetchContext); + expect(mockGetStats).toBeCalledTimes(1); + expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.callCluster, mockIndex); + expect(fetchResult).toBe(mockStats); + }); +}); diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts new file mode 100644 index 0000000000000..4188f564ed5fd --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { getStats, VisualizationUsage } from './get_usage_collector'; + +export function registerVisualizationsCollector( + collectorSet: UsageCollectionSetup, + config: Observable<{ kibana: { index: string } }> +) { + const collector = collectorSet.makeUsageCollector({ + type: 'visualization_types', + isReady: () => true, + schema: { + DYNAMIC_KEY: { + total: { type: 'long' }, + spaces_min: { type: 'long' }, + spaces_max: { type: 'long' }, + spaces_avg: { type: 'long' }, + saved_7_days_total: { type: 'long' }, + saved_30_days_total: { type: 'long' }, + saved_90_days_total: { type: 'long' }, + }, + }, + fetch: async ({ callCluster }) => { + const index = (await config.pipe(first()).toPromise()).kibana.index; + return await getStats(callCluster, index); + }, + }); + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 00fa6e74f952a..6cc8f5c26584a 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -45,6 +45,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; export type PureVisState = SavedVisState; @@ -131,7 +132,14 @@ export interface ByValueVisInstance { export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; +export type VisEditorConstructor = new ( + element: HTMLElement, + vis: Vis, + eventEmitter: EventEmitter, + embeddableHandler: VisualizeEmbeddableContract +) => IEditorController; + export interface IEditorController { - render(props: EditorRenderProps): void; + render(props: EditorRenderProps): Promise | void; destroy(): void; } diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index c5cfa5a4c639b..6010c4f8b163e 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -35,7 +35,12 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices; + const { + data, + createVisEmbeddableFromObject, + savedObjects, + savedObjectsPublic, + } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,10 +60,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( if (vis.data.savedSearchId) { savedSearch = await createSavedSearchesLoader({ savedObjectsClient: savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome, - overlays, + savedObjects: savedObjectsPublic, }).get(vis.data.savedSearchId); } diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index ce0f5fe965d7d..3f9676a9c9385 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -116,6 +116,7 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); @@ -129,10 +130,12 @@ describe('useSavedVisInstance', () => { }); test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { - const { unmount, waitForNextUpdate } = renderHook(() => + const { result, unmount, waitForNextUpdate } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); + await waitForNextUpdate(); unmount(); @@ -158,6 +161,8 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, undefined) ); + result.current.visEditorRef.current = document.createElement('div'); + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, { indexPattern: '1a2b3c4d', type: 'area', diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index ec815b8cfcbee..44fbcce82f458 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -44,7 +44,7 @@ export const useSavedVisInstance = ( savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); - const visEditorRef = useRef(null); + const visEditorRef = useRef(null); const visId = useRef(''); useEffect(() => { @@ -102,16 +102,18 @@ export const useSavedVisInstance = ( let visEditorController; // do not create editor in embeded mode - if (isChromeVisible) { - const Editor = vis.type.editor || DefaultEditorController; - visEditorController = new Editor( - visEditorRef.current, - vis, - eventEmitter, - embeddableHandler - ); - } else if (visEditorRef.current) { - embeddableHandler.render(visEditorRef.current); + if (visEditorRef.current) { + if (isChromeVisible) { + const Editor = vis.type.editor || DefaultEditorController; + visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + } else { + embeddableHandler.render(visEditorRef.current); + } } setState({ diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index f2758d0cc01a4..e0286a63b9feb 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -41,7 +41,7 @@ export const useVisByValue = ( useEffect(() => { const { chrome } = services; const getVisInstance = async () => { - if (!valueInput || loaded.current) { + if (!valueInput || loaded.current || !visEditorRef.current) { return; } const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index d437cadad9fab..246806f300800 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -20,7 +20,11 @@ import { PluginInitializerContext } from 'kibana/public'; import { VisualizePlugin } from './plugin'; -export { EditorRenderProps } from './application/types'; +export type { + EditorRenderProps, + IEditorController, + VisEditorConstructor, +} from './application/types'; export { VisualizeConstants } from './application/visualize_constants'; export const plugin = (context: PluginInitializerContext) => { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 86159a13379a1..ef7d8ea189024 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -49,7 +49,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; import { DashboardStart } from '../../dashboard/public'; -import { UiActionsStart, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public'; +import { UiActionsSetup, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public'; import { setUISettings, setApplication, @@ -69,7 +69,6 @@ export interface VisualizePluginStartDependencies { urlForwarding: UrlForwardingStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; - uiActions: UiActionsStart; } export interface VisualizePluginSetupDependencies { @@ -77,6 +76,7 @@ export interface VisualizePluginSetupDependencies { urlForwarding: UrlForwardingSetup; data: DataPublicPluginSetup; share?: SharePluginSetup; + uiActions: UiActionsSetup; } export class VisualizePlugin @@ -90,7 +90,7 @@ export class VisualizePlugin public async setup( core: CoreSetup, - { home, urlForwarding, data, share }: VisualizePluginSetupDependencies + { home, urlForwarding, data, share, uiActions }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -135,6 +135,7 @@ export class VisualizePlugin ); } setUISettings(core.uiSettings); + uiActions.addTriggerAction(VISUALIZE_FIELD_TRIGGER, visualizeFieldAction); core.application.register({ id: 'visualize', @@ -236,7 +237,6 @@ export class VisualizePlugin if (plugins.share) { setShareService(plugins.share); } - plugins.uiActions.addTriggerAction(VISUALIZE_FIELD_TRIGGER, visualizeFieldAction); } stop() { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts new file mode 100644 index 0000000000000..1f703c64bbde3 --- /dev/null +++ b/test/accessibility/apps/kibana_overview.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana overview', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('kibanaOverview'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.removeSampleDataSet('flights'); + await esArchiver.unload('empty_kibana'); + }); + + it('Getting started view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Overview view', async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('kibanaOverview'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 9068a7e06defc..9730eae1e1360 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -36,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/console'), require.resolve('./apps/home'), require.resolve('./apps/filter_panel'), + require.resolve('./apps/kibana_overview'), ], pageObjects, services, diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 6d4aa1491278e..aaecdbeb0e95d 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -33,6 +33,14 @@ export function analyzeWithAxe(context, options, callback) { id: 'aria-required-children', selector: '[data-skip-axe="aria-required-children"] > *', }, + { + id: 'label', + selector: '[data-test-subj="comboBoxSearchInput"] *', + }, + { + id: 'aria-roles', + selector: '[data-test-subj="comboBoxSearchInput"] *', + }, ], }); return window.axe.run(context, options); diff --git a/test/common/config.js b/test/common/config.js index dbbd75d1f9577..9d6d531ae4b37 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -48,6 +48,8 @@ export default function () { `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, `--home.disableWelcomeScreen=true`, + // Needed for async search functional tests to introduce a delay + `--data.search.aggs.shardDelay.enabled=true`, `--security.showInsecureClusterWarning=false`, '--telemetry.banner=false', '--telemetry.optIn=false', diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index f722e91dc0ae2..b62907dfe2c24 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -57,6 +57,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./time_zones')); loadTestFile(require.resolve('./dashboard_options')); loadTestFile(require.resolve('./data_shared_attributes')); + loadTestFile(require.resolve('./share')); loadTestFile(require.resolve('./embed_mode')); loadTestFile(require.resolve('./dashboard_back_button')); loadTestFile(require.resolve('./dashboard_error_handling')); diff --git a/test/functional/apps/dashboard/share.ts b/test/functional/apps/dashboard/share.ts new file mode 100644 index 0000000000000..cc9c4786f0592 --- /dev/null +++ b/test/functional/apps/dashboard/share.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['dashboard', 'common', 'share']); + + describe('share dashboard', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + }); + + it('has "panels" state when sharing a snapshot', async () => { + await PageObjects.share.clickShareTopNavButton(); + const sharedUrl = await PageObjects.share.getSharedUrl(); + expect(sharedUrl).to.contain('panels'); + }); + }); +} diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index f0472fb5a3da5..d9cb09432b26f 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - describe('discover tab', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/78689 + describe.skip('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index c95211e98cdba..1a1631b9db48b 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -32,7 +32,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover field visualize button', () => { + describe('discover field visualize button', function () { + // unskipped on cloud as these tests test the navigation + // from Discover to Visualize which happens only on OSS + this.tags(['skipCloud']); before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 94409a94e9257..56c6485624043 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const toasts = getService('toasts'); - describe('shared links', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/80104 + describe.skip('shared links', function describeIndexTests() { let baseUrl; async function setup({ storeStateInSessionStorage }) { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 41667e1f26c8e..cc229ef0c2e08 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -505,6 +505,16 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async scrollKibanaBodyTop() { await browser.setScrollToById('kibana-body', 0, 0); } + + /** + * Dismiss Banner if available. + */ + async dismissBanner() { + if (await testSubjects.exists('global-banner-item')) { + const button = await find.byButtonText('Dismiss'); + await button.click(); + } + } } return new CommonPage(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index e522f41952a49..60532c81493f9 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -27,10 +27,10 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider const { header } = getPageObjects(['header']); const browser = getService('browser'); const globalNav = getService('globalNav'); - const config = getService('config'); - const defaultFindTimeout = config.get('timeouts.find'); const elasticChart = getService('elasticChart'); const docTable = getService('docTable'); + const config = getService('config'); + const defaultFindTimeout = config.get('timeouts.find'); class DiscoverPage { public async getChartTimespan() { @@ -84,8 +84,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async waitUntilSearchingHasFinished() { - const spinner = await testSubjects.find('loadingSpinner'); - await find.waitForElementHidden(spinner, defaultFindTimeout * 10); + await testSubjects.missingOrFail('loadingSpinner', { timeout: defaultFindTimeout * 10 }); } public async getColumnHeaders() { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 98ab1babd60fe..de895918efbba 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -124,9 +124,10 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await comboBox.set('filterOperatorList', operator); const params = await testSubjects.find('filterParams'); const paramsComboBoxes = await params.findAllByCssSelector( - '[data-test-subj~="filterParamsComboBox"]' + '[data-test-subj~="filterParamsComboBox"]', + 1000 ); - const paramFields = await params.findAllByTagName('input'); + const paramFields = await params.findAllByTagName('input', 1000); for (let i = 0; i < values.length; i++) { let fieldValues = values[i]; if (!Array.isArray(fieldValues)) { diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index 4252008dcd7ee..e016ef56802f3 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -58,12 +58,15 @@ export class DataSearchTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const service = await data.search.searchSource.asScoped(req); + const savedObjectsClient = savedObjects.getScopedClient(req); // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. - const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory(req); + const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient + ); const ids = await indexPatterns.getIds(); // @ts-expect-error Force overwriting the request req.body.index = ids[0]; diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index ddf9acb259983..a54502b740211 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -39,8 +39,9 @@ export class IndexPatternsTestPlugin router.get( { path: '/api/index-patterns-plugin/get-all', validate: false }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const [{ savedObjects }, { data }] = await core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ids = await service.getIds(); return res.ok({ body: ids }); } @@ -57,8 +58,9 @@ export class IndexPatternsTestPlugin }, async (context, req, res) => { const id = (req.params as Record).id; - const [, { data }] = await core.getStartServices(); - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const [{ savedObjects }, { data }] = await core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); return res.ok({ body: ip.toSpec() }); } @@ -74,9 +76,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); await service.updateSavedObject(ip); return res.ok(); @@ -93,9 +96,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); await service.delete(id); return res.ok(); } diff --git a/test/scripts/checks/bundle_limits.sh b/test/scripts/checks/bundle_limits.sh new file mode 100755 index 0000000000000..10d9d9343fda4 --- /dev/null +++ b/test/scripts/checks/bundle_limits.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +node scripts/build_kibana_platform_plugins --validate-limits diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh index b7eac33f35176..53626ce89462a 100755 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -3,11 +3,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - - destDir="build/kibana-build-oss" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi + destDir="$WORKSPACE/kibana-build-oss-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh index 74a3de77e3a76..b9227fd8ff416 100755 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - - destDir="build/kibana-build-xpack" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi + destDir="$WORKSPACE/kibana-build-xpack-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh index 3fd3d02de1304..289e64f66c89b 100755 --- a/test/scripts/jenkins_xpack_build_plugins.sh +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -10,5 +10,6 @@ node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ --workers 12 \ --verbose diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index ec3dbd919fed6..fd5412c905683 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -87,15 +87,6 @@ def getLatestBuildInfo(comment) { return comment ? getBuildInfoFromComment(comment.body) : null } -def createBuildInfo() { - return [ - status: buildUtils.getBuildStatus(), - url: env.BUILD_URL, - number: env.BUILD_NUMBER, - commit: getCommitHash() - ] -} - def getHistoryText(builds) { if (!builds || builds.size() < 1) { return "" @@ -155,6 +146,16 @@ def getTestFailuresMessage() { return messages.join("\n") } +def getBuildStatusIncludingMetrics() { + def status = buildUtils.getBuildStatus() + + if (status == 'SUCCESS' && !ciStats.getMetricsSuccess()) { + return 'FAILURE' + } + + return status +} + def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { def info = previousCommentInfo ?: [:] info.builds = previousCommentInfo.builds ?: [] @@ -163,7 +164,10 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { info.builds = info.builds.findAll { it.number != env.BUILD_NUMBER } def messages = [] - def status = buildUtils.getBuildStatus() + + def status = isFinal + ? getBuildStatusIncludingMetrics() + : buildUtils.getBuildStatus() if (!isFinal) { def failuresPart = status != 'SUCCESS' ? ', with failures' : '' @@ -228,7 +232,12 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << "To update your PR or re-run it, just comment with:\n`@elasticmachine merge upstream`" - info.builds << createBuildInfo() + info.builds << [ + status: status, + url: env.BUILD_URL, + number: env.BUILD_NUMBER, + commit: getCommitHash() + ] messages << """