diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 8ad810717d86e..8e9b041d32d3e 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -13,7 +13,7 @@ pipeline { BASE_DIR = 'src/github.com/elastic/kibana' HOME = "${env.WORKSPACE}" APM_ITS = 'apm-integration-testing' - CYPRESS_DIR = 'x-pack/legacy/plugins/apm/e2e' + CYPRESS_DIR = 'x-pack/plugins/apm/e2e' PIPELINE_LOG_LEVEL = 'DEBUG' } options { @@ -39,7 +39,7 @@ pipeline { shallow: false, reference: "/var/lib/jenkins/.git-references/kibana.git") script { dir("${BASE_DIR}"){ - def regexps =[ "^x-pack/legacy/plugins/apm/.*" ] + def regexps =[ "^x-pack/plugins/apm/.*" ] env.APM_UPDATED = isGitRegionMatch(patterns: regexps) } } diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 75b686abe845f..c87ca01354315 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,7 +19,7 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -kibanaPipeline(timeoutMinutes: 120) { +kibanaPipeline(timeoutMinutes: 150) { catchErrors { slackNotifications.onFailure( title: ":broken_heart: *<${env.BUILD_URL}|[${SNAPSHOT_VERSION}] ES Snapshot Verification Failure>*", diff --git a/.eslintignore b/.eslintignore index 4913192e81c1d..362b3e42d48e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,11 +25,11 @@ target /src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** -/x-pack/legacy/plugins/apm/e2e/cypress/**/snapshots.js -/x-pack/legacy/plugins/canvas/canvas_plugin -/x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts -/x-pack/legacy/plugins/canvas/shareable_runtime/build -/x-pack/legacy/plugins/canvas/storybook +/x-pack/plugins/apm/e2e/cypress/**/snapshots.js +/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 /x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8b33ec83347a8..dde0ce010d4d4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -89,7 +89,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/canvas/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/canvas/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', 'jsx-a11y/click-events-have-key-events': 'off', @@ -151,6 +151,16 @@ module.exports = { }, }, + /** + * New Platform client-side + */ + { + files: ['{src,x-pack}/plugins/*/public/**/*.{js,ts,tsx}'], + rules: { + 'import/no-commonjs': 'error', + }, + }, + /** * Files that require Elastic license headers instead of Apache 2.0 header */ @@ -183,6 +193,11 @@ module.exports = { { basePath: __dirname, zones: [ + { + target: ['(src|x-pack)/**/*', '!src/core/**/*'], + from: ['src/core/utils/**/*'], + errorMessage: `Plugins may only import from src/core/server and src/core/public.`, + }, { target: [ '(src|x-pack)/legacy/**/*', @@ -306,7 +321,7 @@ module.exports = { { files: [ 'x-pack/test/functional/apps/**/*.js', - 'x-pack/legacy/plugins/apm/**/*.js', + 'x-pack/plugins/apm/**/*.js', 'test/*/config.ts', 'test/*/config_open.ts', 'test/*/{tests,test_suites,apis,apps}/**/*', @@ -393,7 +408,7 @@ module.exports = { 'x-pack/**/*.test.js', 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', - 'x-pack/legacy/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/apm/public/utils/testHelpers.js', ], rules: { 'import/no-extraneous-dependencies': [ @@ -519,7 +534,7 @@ module.exports = { * APM overrides */ { - files: ['x-pack/legacy/plugins/apm/**/*.js'], + files: ['x-pack/plugins/apm/**/*.js'], rules: { 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 'no-console': ['warn', { allow: ['error'] }], @@ -527,7 +542,7 @@ module.exports = { }, { plugins: ['react-hooks'], - files: ['x-pack/legacy/plugins/apm/**/*.{ts,tsx}'], + files: ['x-pack/plugins/apm/**/*.{ts,tsx}'], rules: { 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/exhaustive-deps': ['error', { additionalHooks: '^useFetcher$' }], @@ -878,7 +893,7 @@ module.exports = { * Canvas overrides */ { - files: ['x-pack/legacy/plugins/canvas/**/*.js'], + files: ['x-pack/plugins/canvas/**/*.js'], rules: { radix: 'error', @@ -922,12 +937,12 @@ module.exports = { }, { files: [ - 'x-pack/legacy/plugins/canvas/gulpfile.js', - 'x-pack/legacy/plugins/canvas/scripts/*.js', - 'x-pack/legacy/plugins/canvas/tasks/*.js', - 'x-pack/legacy/plugins/canvas/tasks/**/*.js', - 'x-pack/legacy/plugins/canvas/__tests__/**/*.js', - 'x-pack/legacy/plugins/canvas/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__}/**/*.js', + 'x-pack/plugins/canvas/gulpfile.js', + 'x-pack/plugins/canvas/scripts/*.js', + 'x-pack/plugins/canvas/tasks/*.js', + 'x-pack/plugins/canvas/tasks/**/*.js', + 'x-pack/plugins/canvas/__tests__/**/*.js', + 'x-pack/plugins/canvas/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__}/**/*.js', ], rules: { 'import/no-extraneous-dependencies': [ @@ -940,7 +955,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/canvas/canvas_plugin_src/**/*.js'], + files: ['x-pack/plugins/canvas/canvas_plugin_src/**/*.js'], globals: { canvas: true, $: true }, rules: { 'import/no-unresolved': [ @@ -952,13 +967,13 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/canvas/public/**/*.js'], + files: ['x-pack/plugins/canvas/public/**/*.js'], env: { browser: true, }, }, { - files: ['x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts/**/*.js'], + files: ['x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/**/*.js'], env: { jquery: true, }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6e616cf78c206..638e86ef375fe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -66,7 +66,7 @@ /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch # APM -/x-pack/legacy/plugins/apm/ @elastic/apm-ui +/x-pack/plugins/apm/ @elastic/apm-ui /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui @@ -77,7 +77,7 @@ /x-pack/legacy/plugins/beats_management/ @elastic/beats # Canvas -/x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas +/x-pack/plugins/canvas/ @elastic/kibana-canvas # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 89e0af270c54d..d9d99fc1416e4 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -8,9 +8,8 @@ - "Feature:ExpressionLanguage": - "src/plugins/expressions/**/*.*" - "src/plugins/bfetch/**/*.*" - - "Team:apm" + - "Team:apm": + - "x-pack/plugins/apm/**/*.*" - "x-pack/plugins/apm/**/*.*" - - "x-pack/legacy/plugins/apm/**/*.*" - "Team:uptime": - "x-pack/plugins/uptime/**/*.*" - - "x-pack/legacy/plugins/uptime/**/*.*" diff --git a/.gitignore b/.gitignore index bd7a954f950e9..13c7cd5fb2769 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,6 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/legacy/plugins/apm/tsconfig.json +x-pack/plugins/apm/tsconfig.json apm.tsconfig.json -/x-pack/legacy/plugins/apm/e2e/snapshots.js +/x-pack/plugins/apm/e2e/snapshots.js diff --git a/.sass-lint.yml b/.sass-lint.yml index 44b4d49384136..c8985108dabf2 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -5,14 +5,14 @@ files: - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' + - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' - 'x-pack/plugins/cross_cluster_replication/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: - - 'x-pack/legacy/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' + - 'x-pack/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' rules: quotes: - 2 diff --git a/config/kibana.yml b/config/kibana.yml index 0780841ca057e..8725888159506 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -40,7 +40,7 @@ # the username and password that the Kibana server uses to perform maintenance on the Kibana # index at startup. Your Kibana users still need to authenticate with Elasticsearch, which # is proxied through the Kibana server. -#elasticsearch.username: "kibana" +#elasticsearch.username: "kibana_system" #elasticsearch.password: "pass" # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index 2eed339160fc4..c35fb115d2db4 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -1,38 +1,53 @@ [role="xpack"] [[spans]] -=== Span timeline +=== Trace sample timeline -TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is the duration of a single event. -Spans are automatically captured by APM agents, and you can also define custom spans. -Each span has a type and is defined by a different color in the timeline/waterfall visualization. - -The span timeline visualization is a bird's-eye view of what your application was doing while it was trying to respond to the request that came in. +The trace sample timeline visualization is a bird's-eye view of what your application was doing while it was trying to respond to a request. This makes it useful for visualizing where the selected transaction spent most of its time. [role="screenshot"] image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana] View a span in detail by clicking on it in the timeline waterfall. -When you click on an SQL Select database query, +For example, when you click on an SQL Select database query, the information displayed includes the actual SQL that was executed, how long it took, and the percentage of the trace's total time. You also get a stack trace, which shows the SQL query in your code. Finally, APM knows which files are your code and which are just modules or libraries that you've installed. These library frames will be minimized by default in order to show you the most relevant stack trace. +TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is the duration of a single event. +Spans are automatically captured by APM agents, and you can also define custom spans. +Each span has a type and is defined by a different color in the timeline/waterfall visualization. + [role="screenshot"] image::apm/images/apm-span-detail.png[Example view of a span detail in the APM app in Kibana] -If your span timeline is colorful, it's indicative of a <>. +[float] +[[distributed-tracing]] +==== Distributed tracing + +If your trace sample timeline is colorful, it's indicative of a distributed trace. Services in a distributed trace are separated by color and listed in the order they occur. [role="screenshot"] image::apm/images/apm-services-trace.png[Example of distributed trace colors in the APM app in Kibana] -Don't forget; a distributed trace includes more than one transaction. +As application architectures are shifting from monolithic to more distributed, service-based architectures, +distributed tracing has become a crucial feature of modern application performance monitoring. +It allows you to trace requests through your service architecture automatically, and visualize those traces in one single view in the APM app. +From initial web requests to your front-end service, to queries made to your back-end services, +this makes finding possible bottlenecks throughout your application much easier and faster. + +[role="screenshot"] +image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] + +Don't forget; by definition, a distributed trace includes more than one transaction. When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon, which indicates the next transaction in the trace. These transactions can be expanded and viewed in detail by clicking on them. After exploring these traces, you can return to the full trace by clicking *View full trace*. + +TIP: Distributed tracing is supported by all APM agents, and there's no additional configuration needed. diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 8eef3d9bed4db..52b4b618de466 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -4,7 +4,7 @@ TIP: Traces link together related transactions to show an end-to-end performance of how a request was served and which services were part of it. -In addition to the Traces overview, you can view your application traces in the <>. +In addition to the Traces overview, you can view your application traces in the <>. The *Traces* overview displays the entry transaction for all traces in your application. If you're using <>, this view is key to finding the critical paths within your application. @@ -17,25 +17,3 @@ If there's a particular endpoint you're worried about, you can click on it to vi [role="screenshot"] image::apm/images/apm-traces.png[Example view of the Traces overview in APM app in Kibana] - -[float] -[[distributed-tracing]] -==== Distributed tracing - -Elastic APM supports distributed tracing. -Distributed tracing is a key feature of modern application performance monitoring as application architectures are shifting from monolithic to more distributed, -service-based architectures. - -Distributed tracing allows APM users to automatically trace requests all the way through the service architecture, -and visualize those traces in one single view in the APM app. -This is accomplished by tracing all of the requests, from the initial web request to your front-end service, -to queries made to your back-end services. -This makes finding possible bottlenecks throughout your application much easier and faster. - -By definition, a distributed trace includes more than one transaction. -You can use the <> to view a waterfall display of all of the transactions from individual services that are connected in a trace. - -[role="screenshot"] -image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] - -TIP: Distributed tracing is supported by all APM agents, and there's no additional configuration needed. \ No newline at end of file diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 2e1022e6d684c..8012c9108ca5e 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -95,7 +95,7 @@ It's the requests on the right, the ones taking longer than average, that we pro When you select one of these buckets, you're presented with up to ten trace samples. -Each sample has a span timeline waterfall that shows what a typical request in that bucket was doing. +Each sample has a trace timeline waterfall that shows what a typical request in that bucket was doing. By investigating this timeline waterfall, we can hopefully determine _why_ this request was slow and then implement a fix. [role="screenshot"] diff --git a/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md b/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md new file mode 100644 index 0000000000000..0342a1d9ee95b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppCategory](./kibana-plugin-core-public.appcategory.md) > [id](./kibana-plugin-core-public.appcategory.id.md) + +## AppCategory.id property + +Unique identifier for the categories + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appcategory.md b/docs/development/core/public/kibana-plugin-core-public.appcategory.md index b115baa1be1a3..d91727a1bbf29 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appcategory.md +++ b/docs/development/core/public/kibana-plugin-core-public.appcategory.md @@ -18,6 +18,7 @@ export interface AppCategory | --- | --- | --- | | [ariaLabel](./kibana-plugin-core-public.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | | [euiIconType](./kibana-plugin-core-public.appcategory.euiicontype.md) | string | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | +| [id](./kibana-plugin-core-public.appcategory.id.md) | string | Unique identifier for the categories | | [label](./kibana-plugin-core-public.appcategory.label.md) | string | Label used for cateogry name. Also used as aria-label if one isn't set. | | [order](./kibana-plugin-core-public.appcategory.order.md) | number | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | diff --git a/docs/development/core/public/kibana-plugin-core-public.assertnever.md b/docs/development/core/public/kibana-plugin-core-public.assertnever.md new file mode 100644 index 0000000000000..8fefd4450d49b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.assertnever.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [assertNever](./kibana-plugin-core-public.assertnever.md) + +## assertNever() function + +Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking + +Signature: + +```typescript +export declare function assertNever(x: never): never; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| x | never | | + +Returns: + +`never` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md new file mode 100644 index 0000000000000..09864be43996d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getNavType$](./kibana-plugin-core-public.chromestart.getnavtype_.md) + +## ChromeStart.getNavType$() method + +Get the navigation type TODO \#64541 Can delete + +Signature: + +```typescript +getNavType$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index c179e089d7cfd..b4eadc93fe78d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -58,6 +58,7 @@ core.chrome.setHelpExtension(elem => { | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | +| [getNavType$()](./kibana-plugin-core-public.chromestart.getnavtype_.md) | Get the navigation type TODO \#64541 Can delete | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | diff --git a/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md b/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md new file mode 100644 index 0000000000000..7c879b659a852 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deepfreeze.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [deepFreeze](./kibana-plugin-core-public.deepfreeze.md) + +## deepFreeze() function + +Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively + +Signature: + +```typescript +export declare function deepFreeze(object: T): RecursiveReadonly; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| object | T | | + +Returns: + +`RecursiveReadonly` + diff --git a/docs/development/core/public/kibana-plugin-core-public.freezable.md b/docs/development/core/public/kibana-plugin-core-public.freezable.md new file mode 100644 index 0000000000000..fee87dde25c28 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.freezable.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Freezable](./kibana-plugin-core-public.freezable.md) + +## Freezable type + + +Signature: + +```typescript +export declare type Freezable = { + [k: string]: any; +} | any[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md b/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md new file mode 100644 index 0000000000000..3ef9b6bf703eb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.getflattenedobject.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [getFlattenedObject](./kibana-plugin-core-public.getflattenedobject.md) + +## getFlattenedObject() function + +Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from `rootValue`. + +example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } + +Signature: + +```typescript +export declare function getFlattenedObject(rootValue: Record): { + [key: string]: any; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rootValue | Record<string, any> | | + +Returns: + +`{ + [key: string]: any; +}` + diff --git a/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md b/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md new file mode 100644 index 0000000000000..3c2ffa6340a97 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.isrelativeurl.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [isRelativeUrl](./kibana-plugin-core-public.isrelativeurl.md) + +## isRelativeUrl() function + +Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* + +Signature: + +```typescript +export declare function isRelativeUrl(candidatePath: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| candidatePath | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index adc87de2b9e7e..eafc81447ee03 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -27,6 +27,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | Status of the application's navLink. | | [AppStatus](./kibana-plugin-core-public.appstatus.md) | Accessibility status of an application. | +## Functions + +| Function | Description | +| --- | --- | +| [assertNever(x)](./kibana-plugin-core-public.assertnever.md) | Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking | +| [deepFreeze(object)](./kibana-plugin-core-public.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | +| [getFlattenedObject(rootValue)](./kibana-plugin-core-public.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | +| [isRelativeUrl(candidatePath)](./kibana-plugin-core-public.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | +| [modifyUrl(url, urlModifier)](./kibana-plugin-core-public.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | + ## Interfaces | Interface | Description | @@ -118,6 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) APIs. | | [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsState](./kibana-plugin-core-public.uisettingsstate.md) | | +| [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) | We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". | | [UserProvidedValues](./kibana-plugin-core-public.userprovidedvalues.md) | Describes the values explicitly set by user. | ## Type Aliases @@ -139,6 +150,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-core-public.chromenavlinkupdateablefields.md) | | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | +| [Freezable](./kibana-plugin-core-public.freezable.md) | | | [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-core-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md). | @@ -146,6 +158,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | +| [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | diff --git a/docs/development/core/public/kibana-plugin-core-public.modifyurl.md b/docs/development/core/public/kibana-plugin-core-public.modifyurl.md new file mode 100644 index 0000000000000..b174f733a5c64 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.modifyurl.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [modifyUrl](./kibana-plugin-core-public.modifyurl.md) + +## modifyUrl() function + +Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url. + +Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hash + +Why? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints + +Signature: + +```typescript +export declare function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| url | string | | +| urlModifier | (urlParts: URLMeaningfulParts) => Partial<URLMeaningfulParts> | void | | + +Returns: + +`string` + +The modified and reformatted url + diff --git a/docs/development/core/public/kibana-plugin-core-public.navtype.md b/docs/development/core/public/kibana-plugin-core-public.navtype.md new file mode 100644 index 0000000000000..8f1d9a4351754 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.navtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavType](./kibana-plugin-core-public.navtype.md) + +## NavType type + +Signature: + +```typescript +export declare type NavType = 'modern' | 'legacy'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md new file mode 100644 index 0000000000000..238dd66885896 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.auth.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [auth](./kibana-plugin-core-public.urlmeaningfulparts.auth.md) + +## URLMeaningfulParts.auth property + +Signature: + +```typescript +auth?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md new file mode 100644 index 0000000000000..161e7dc7ebfae --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hash.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [hash](./kibana-plugin-core-public.urlmeaningfulparts.hash.md) + +## URLMeaningfulParts.hash property + +Signature: + +```typescript +hash?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md new file mode 100644 index 0000000000000..f1884718337b5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.hostname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [hostname](./kibana-plugin-core-public.urlmeaningfulparts.hostname.md) + +## URLMeaningfulParts.hostname property + +Signature: + +```typescript +hostname?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md new file mode 100644 index 0000000000000..2816d4c7df541 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) + +## URLMeaningfulParts interface + +We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". + +Signature: + +```typescript +export interface URLMeaningfulParts +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [auth](./kibana-plugin-core-public.urlmeaningfulparts.auth.md) | string | null | | +| [hash](./kibana-plugin-core-public.urlmeaningfulparts.hash.md) | string | null | | +| [hostname](./kibana-plugin-core-public.urlmeaningfulparts.hostname.md) | string | null | | +| [pathname](./kibana-plugin-core-public.urlmeaningfulparts.pathname.md) | string | null | | +| [port](./kibana-plugin-core-public.urlmeaningfulparts.port.md) | string | null | | +| [protocol](./kibana-plugin-core-public.urlmeaningfulparts.protocol.md) | string | null | | +| [query](./kibana-plugin-core-public.urlmeaningfulparts.query.md) | ParsedQuery | | +| [slashes](./kibana-plugin-core-public.urlmeaningfulparts.slashes.md) | boolean | null | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md new file mode 100644 index 0000000000000..5ad21f004481c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.pathname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [pathname](./kibana-plugin-core-public.urlmeaningfulparts.pathname.md) + +## URLMeaningfulParts.pathname property + +Signature: + +```typescript +pathname?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md new file mode 100644 index 0000000000000..2e70da2f17421 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.port.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [port](./kibana-plugin-core-public.urlmeaningfulparts.port.md) + +## URLMeaningfulParts.port property + +Signature: + +```typescript +port?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md new file mode 100644 index 0000000000000..cedc7f0b878e3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.protocol.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [protocol](./kibana-plugin-core-public.urlmeaningfulparts.protocol.md) + +## URLMeaningfulParts.protocol property + +Signature: + +```typescript +protocol?: string | null; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md new file mode 100644 index 0000000000000..a9541efe0882a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.query.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [query](./kibana-plugin-core-public.urlmeaningfulparts.query.md) + +## URLMeaningfulParts.query property + +Signature: + +```typescript +query: ParsedQuery; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md new file mode 100644 index 0000000000000..cb28a25f9e162 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.urlmeaningfulparts.slashes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [URLMeaningfulParts](./kibana-plugin-core-public.urlmeaningfulparts.md) > [slashes](./kibana-plugin-core-public.urlmeaningfulparts.slashes.md) + +## URLMeaningfulParts.slashes property + +Signature: + +```typescript +slashes?: boolean | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.assertnever.md b/docs/development/core/server/kibana-plugin-core-server.assertnever.md new file mode 100644 index 0000000000000..c13c88df9b9bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.assertnever.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [assertNever](./kibana-plugin-core-server.assertnever.md) + +## assertNever() function + +Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking + +Signature: + +```typescript +export declare function assertNever(x: never): never; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| x | never | | + +Returns: + +`never` + diff --git a/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md b/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md new file mode 100644 index 0000000000000..946050bff0585 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deepfreeze.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [deepFreeze](./kibana-plugin-core-server.deepfreeze.md) + +## deepFreeze() function + +Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively + +Signature: + +```typescript +export declare function deepFreeze(object: T): RecursiveReadonly; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| object | T | | + +Returns: + +`RecursiveReadonly` + diff --git a/docs/development/core/server/kibana-plugin-core-server.freezable.md b/docs/development/core/server/kibana-plugin-core-server.freezable.md new file mode 100644 index 0000000000000..32ba89e8370c1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.freezable.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Freezable](./kibana-plugin-core-server.freezable.md) + +## Freezable type + + +Signature: + +```typescript +export declare type Freezable = { + [k: string]: any; +} | any[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md b/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md new file mode 100644 index 0000000000000..2e7850ca579f6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getflattenedobject.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [getFlattenedObject](./kibana-plugin-core-server.getflattenedobject.md) + +## getFlattenedObject() function + +Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from `rootValue`. + +example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } + +Signature: + +```typescript +export declare function getFlattenedObject(rootValue: Record): { + [key: string]: any; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rootValue | Record<string, any> | | + +Returns: + +`{ + [key: string]: any; +}` + diff --git a/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md b/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md new file mode 100644 index 0000000000000..bff9eb05419be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isrelativeurl.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [isRelativeUrl](./kibana-plugin-core-server.isrelativeurl.md) + +## isRelativeUrl() function + +Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* + +Signature: + +```typescript +export declare function isRelativeUrl(candidatePath: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| candidatePath | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a91a5bec988b7..14e01fda3d287 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -41,8 +41,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | Function | Description | | --- | --- | +| [assertNever(x)](./kibana-plugin-core-server.assertnever.md) | Can be used in switch statements to ensure we perform exhaustive checks, see https://www.typescriptlang.org/docs/handbook/advanced-types.html\#exhaustiveness-checking | +| [deepFreeze(object)](./kibana-plugin-core-server.deepfreeze.md) | Apply Object.freeze to a value recursively and convert the return type to Readonly variant recursively | | [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | +| [getFlattenedObject(rootValue)](./kibana-plugin-core-server.getflattenedobject.md) | Flattens a deeply nested object to a map of dot-separated paths pointing to all primitive values \*\*and arrays\*\* from rootValue.example: getFlattenedObject({ a: { b: 1, c: \[2,3\] } }) // => { 'a.b': 1, 'a.c': \[2,3\] } | | [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [isRelativeUrl(candidatePath)](./kibana-plugin-core-server.isrelativeurl.md) | Determine if a url is relative. Any url including a protocol, hostname, or port is not considered relative. This means that absolute \*paths\* are considered to be relative \*urls\* | +| [modifyUrl(url, urlModifier)](./kibana-plugin-core-server.modifyurl.md) | Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url.Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hashWhy? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints | | [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | ## Interfaces @@ -186,6 +191,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | | | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | | +| [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) | We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". | | [UserProvidedValues](./kibana-plugin-core-server.userprovidedvalues.md) | Describes the values explicitly set by user. | | [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) | APIs to access the application's instance uuid. | @@ -212,6 +218,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigPath](./kibana-plugin-core-server.configpath.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | | +| [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | | [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. | diff --git a/docs/development/core/server/kibana-plugin-core-server.modifyurl.md b/docs/development/core/server/kibana-plugin-core-server.modifyurl.md new file mode 100644 index 0000000000000..fc0bc354a3ca3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.modifyurl.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [modifyUrl](./kibana-plugin-core-server.modifyurl.md) + +## modifyUrl() function + +Takes a URL and a function that takes the meaningful parts of the URL as a key-value object, modifies some or all of the parts, and returns the modified parts formatted again as a url. + +Url Parts sent: - protocol - slashes (does the url have the //) - auth - hostname (just the name of the host, no port or auth information) - port - pathname (the path after the hostname, no query or hash, starts with a slash if there was a path) - query (always an object, even when no query on original url) - hash + +Why? - The default url library in node produces several conflicting properties on the "parsed" output. Modifying any of these might lead to the modifications being ignored (depending on which property was modified) - It's not always clear whether to use path/pathname, host/hostname, so this tries to add helpful constraints + +Signature: + +```typescript +export declare function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| url | string | | +| urlModifier | (urlParts: URLMeaningfulParts) => Partial<URLMeaningfulParts> | void | | + +Returns: + +`string` + +The modified and reformatted url + diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md new file mode 100644 index 0000000000000..0422738669a70 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.auth.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [auth](./kibana-plugin-core-server.urlmeaningfulparts.auth.md) + +## URLMeaningfulParts.auth property + +Signature: + +```typescript +auth?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md new file mode 100644 index 0000000000000..13a3f4a9c95c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hash.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [hash](./kibana-plugin-core-server.urlmeaningfulparts.hash.md) + +## URLMeaningfulParts.hash property + +Signature: + +```typescript +hash?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md new file mode 100644 index 0000000000000..6631f6f6744c5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.hostname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [hostname](./kibana-plugin-core-server.urlmeaningfulparts.hostname.md) + +## URLMeaningfulParts.hostname property + +Signature: + +```typescript +hostname?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md new file mode 100644 index 0000000000000..257f7b4b634ab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) + +## URLMeaningfulParts interface + +We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". + +Signature: + +```typescript +export interface URLMeaningfulParts +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [auth](./kibana-plugin-core-server.urlmeaningfulparts.auth.md) | string | null | | +| [hash](./kibana-plugin-core-server.urlmeaningfulparts.hash.md) | string | null | | +| [hostname](./kibana-plugin-core-server.urlmeaningfulparts.hostname.md) | string | null | | +| [pathname](./kibana-plugin-core-server.urlmeaningfulparts.pathname.md) | string | null | | +| [port](./kibana-plugin-core-server.urlmeaningfulparts.port.md) | string | null | | +| [protocol](./kibana-plugin-core-server.urlmeaningfulparts.protocol.md) | string | null | | +| [query](./kibana-plugin-core-server.urlmeaningfulparts.query.md) | ParsedQuery | | +| [slashes](./kibana-plugin-core-server.urlmeaningfulparts.slashes.md) | boolean | null | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md new file mode 100644 index 0000000000000..8fee8c8e146ca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.pathname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [pathname](./kibana-plugin-core-server.urlmeaningfulparts.pathname.md) + +## URLMeaningfulParts.pathname property + +Signature: + +```typescript +pathname?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md new file mode 100644 index 0000000000000..dcf3517d92ba2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.port.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [port](./kibana-plugin-core-server.urlmeaningfulparts.port.md) + +## URLMeaningfulParts.port property + +Signature: + +```typescript +port?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md new file mode 100644 index 0000000000000..914dcd4e8a8a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.protocol.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [protocol](./kibana-plugin-core-server.urlmeaningfulparts.protocol.md) + +## URLMeaningfulParts.protocol property + +Signature: + +```typescript +protocol?: string | null; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md new file mode 100644 index 0000000000000..358adcfd3d180 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.query.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [query](./kibana-plugin-core-server.urlmeaningfulparts.query.md) + +## URLMeaningfulParts.query property + +Signature: + +```typescript +query: ParsedQuery; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md new file mode 100644 index 0000000000000..d5b598167f2f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.urlmeaningfulparts.slashes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) > [slashes](./kibana-plugin-core-server.urlmeaningfulparts.slashes.md) + +## URLMeaningfulParts.slashes property + +Signature: + +```typescript +slashes?: boolean | null; +``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 51910169e8673..cafd50d92376f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -68,6 +68,9 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). +`pageNavigation`:: The style of navigation menu for Kibana. +Choices are Legacy, the legacy style where every plugin is represented in the nav, +and Modern, a new format that bundles related plugins together in flyaway nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 24aacd6a47626..f4178bacb111e 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -31,14 +31,14 @@ file: [source,yaml] ----------------------------------------------- -elasticsearch.username: "kibana" +elasticsearch.username: "kibana_system" elasticsearch.password: "kibanapassword" ----------------------------------------------- The {kib} server submits requests as this user to access the cluster monitoring APIs and the `.kibana` index. The server does _not_ need access to user indices. -The password for the built-in `kibana` user is typically set as part of the +The password for the built-in `kibana_system` user is typically set as part of the {security} configuration process on {es}. For more information, see {ref}/built-in-users.html[Built-in users]. -- diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc index a7520227977bc..852c3e1ecdeca 100644 --- a/docs/visualize/timelion.asciidoc +++ b/docs/visualize/timelion.asciidoc @@ -50,10 +50,10 @@ To compare the two data sets, add another series with data from the previous hou .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct'), - .es(offset=-1h, <1> - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') +.es(offset=-1h, <1> + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') ---------------------------------- <1> `offset` offsets the data retrieval by a date expression. In this example, `-1h` offsets the data back by one hour. @@ -119,11 +119,11 @@ To differentiate between the current hour data and the last hour data, change th metric='avg:system.cpu.user.pct') .label('last hour') .lines(fill=1,width=0.5), <1> - .es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') ---------------------------------- <1> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=1,width=0.5)` sets the fill level to `1`, and the border width to `0.5`. @@ -169,7 +169,20 @@ Change the position and style of the legend: [source,text] ---------------------------------- -.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour').lines(fill=1,width=0.5).color(gray), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour').title('CPU usage over time').color(#1E90FF).legend(columns=2, position=nw) <1> +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5) + .color(gray), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') + .color(#1E90FF) + .legend(columns=2, position=nw) <1> ---------------------------------- <1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. @@ -192,7 +205,9 @@ To start tracking the inbound and outbound network traffic, enter the following [source,text] ---------------------------------- -.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes) +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) ---------------------------------- [role="screenshot"] @@ -207,7 +222,10 @@ Change how the data is displayed so that you can easily monitor the inbound traf [source,text] ---------------------------------- -.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative() <1> +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() <1> ---------------------------------- <1> `.derivative` plots the change in values over time. @@ -220,7 +238,15 @@ Add a similar calculation for outbound traffic: [source,text] ---------------------------------- -.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative(), .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.out.bytes).derivative().multiply(-1) <1> +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative(), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) <1> ---------------------------------- <1> `.multiply()` multiplies the data series by a number, the result of a data series, or a list of data series. For this example, `.multiply(-1)` converts the outbound network traffic to a negative value since the outbound network traffic is leaving your machine. @@ -237,7 +263,17 @@ To make the visualization easier to analyze, change the data metric from bytes t [source,text] ---------------------------------- -.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative().divide(1048576), .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.out.bytes).derivative().multiply(-1).divide(1048576) <1> +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() + .divide(1048576), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) + .divide(1048576) <1> ---------------------------------- <1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. @@ -271,8 +307,8 @@ Customize and format the visualization using functions: .divide(1048576) .lines(fill=2, width=1) <3> .color(blue) <4> - .label("Outbound traffic") - .legend(columns=2, position=nw) <5> + .label("Outbound traffic") + .legend(columns=2, position=nw) <5> ---------------------------------- <1> `.label()` adds custom labels to the visualization. @@ -309,7 +345,9 @@ To chart the maximum value of `system.memory.actual.used.bytes`, enter the follo [source,text] ---------------------------------- -.es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes') +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') ---------------------------------- [role="screenshot"] @@ -338,17 +376,17 @@ To track the amount of memory used, create two thresholds: null) .label('warning') .color('#FFCC11'), - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') ---------------------------------- <1> Timelion conditional logic for the _greater than_ operator. In this example, the warning threshold is 11.3GB (`11300000000`), and the severe threshold is 11.375GB (`11375000000`). If the threshold values are too high or low for your machine, adjust the values accordingly. @@ -366,7 +404,33 @@ To determine the trend, create a new data series: [source,text] ---------------------------------- -.es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes'), .es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes').if(gt,11300000000,.es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes'),null).label('warning').color('#FFCC11'), .es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes').if(gt,11375000000,.es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes'),null).label('severe').color('red'), .es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes').mvavg(10) <1> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11300000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null). + label('severe') + .color('red'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) <1> ---------------------------------- <1> `mvavg()` calculates the moving average over a specified period of time. In this example, `.mvavg(10)` creates a moving average with a window of 10 data points. @@ -396,30 +460,30 @@ Customize and format the visualization using functions: .es(index=metricbeat-*, timefield='@timestamp', metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11') <3> - .lines(width=5), <4> - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') - .lines(width=5), + null) + .label('warning') + .color('#FFCC11') <3> + .lines(width=5), <4> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, .es(index=metricbeat-*, timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .mvavg(10) - .label('mvavg') - .lines(width=2) - .color(#5E5E5E) - .legend(columns=4, position=nw) <5> + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') + .lines(width=5), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) + .label('mvavg') + .lines(width=2) + .color(#5E5E5E) + .legend(columns=4, position=nw) <5> ---------------------------------- <1> `.label()` adds custom labels to the visualization. diff --git a/package.json b/package.json index 1f0658bd2a138..8a92b46489308 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", "test:coverage": "grunt test:coverage", - "typespec": "typings-tester --config x-pack/legacy/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/legacy/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", + "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", "start": "node scripts/kibana --dev", @@ -122,10 +122,10 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "19.1.2", + "@elastic/charts": "19.2.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "22.3.0", + "@elastic/eui": "22.3.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", @@ -200,7 +200,7 @@ "inert": "^5.1.0", "inline-style": "^2.0.0", "joi": "^13.5.2", - "jquery": "^3.4.1", + "jquery": "^3.5.0", "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", @@ -297,6 +297,7 @@ "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^5.0.1", + "@elastic/static-fs": "1.0.1", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/eslint-import-resolver-kibana": "2.0.0", diff --git a/packages/kbn-dev-utils/src/precommit_hook/cli.ts b/packages/kbn-dev-utils/src/precommit_hook/cli.ts new file mode 100644 index 0000000000000..a83e8c2b193d9 --- /dev/null +++ b/packages/kbn-dev-utils/src/precommit_hook/cli.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 Path from 'path'; +import { chmod, writeFile } from 'fs'; +import { promisify } from 'util'; + +import { run } from '../run'; +import { REPO_ROOT } from '../repo_root'; +import { SCRIPT_SOURCE } from './script_source'; +import { getGitDir } from './get_git_dir'; + +const chmodAsync = promisify(chmod); +const writeFileAsync = promisify(writeFile); + +run( + async ({ log }) => { + try { + const gitDir = await getGitDir(); + const installPath = Path.resolve(REPO_ROOT, gitDir, 'hooks/pre-commit'); + + log.info(`Registering Kibana pre-commit git hook...`); + await writeFileAsync(installPath, SCRIPT_SOURCE); + await chmodAsync(installPath, 0o755); + log.success(`Kibana pre-commit git hook was installed successfully.`); + } catch (e) { + log.error(`Kibana pre-commit git hook was not installed as an error occur.`); + throw e; + } + }, + { + description: 'Register git hooks in the local repo', + } +); diff --git a/test/functional/page_objects/monitoring_page.js b/packages/kbn-dev-utils/src/precommit_hook/get_git_dir.ts similarity index 67% rename from test/functional/page_objects/monitoring_page.js rename to packages/kbn-dev-utils/src/precommit_hook/get_git_dir.ts index 7dab9dc3e52b1..5ca7d67d0d4ea 100644 --- a/test/functional/page_objects/monitoring_page.js +++ b/packages/kbn-dev-utils/src/precommit_hook/get_git_dir.ts @@ -17,19 +17,16 @@ * under the License. */ -export function MonitoringPageProvider({ getService }) { - const find = getService('find'); +import execa from 'execa'; - class MonitoringPage { - async getWelcome() { - const el = await find.displayedByCssSelector('render-directive'); - return await el.getVisibleText(); - } +import { REPO_ROOT } from '../repo_root'; - async clickOptOut() { - return find.clickByLinkText('Opt out here'); - } - } - - return new MonitoringPage(); +// Retrieves the correct location for the .git dir for +// every git setup (including git worktree) +export async function getGitDir() { + return ( + await execa('git', ['rev-parse', '--git-common-dir'], { + cwd: REPO_ROOT, + }) + ).stdout.trim(); } diff --git a/packages/kbn-dev-utils/src/precommit_hook/script_source.ts b/packages/kbn-dev-utils/src/precommit_hook/script_source.ts new file mode 100644 index 0000000000000..61b4552f6eaef --- /dev/null +++ b/packages/kbn-dev-utils/src/precommit_hook/script_source.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import os from 'os'; + +import normalizePath from 'normalize-path'; + +const HOME_DIR = normalizePath(os.homedir()); + +export const SCRIPT_SOURCE = `#!/usr/bin/env bash +# +# ** THIS IS AN AUTO-GENERATED FILE ** +# ** PLEASE DO NOT CHANGE IT MANUALLY ** +# +# GENERATED BY \`node scripts/register_git_hook\` +# IF YOU WANNA CHANGE SOMETHING IN THIS SCRIPT +# PLEASE RE-RUN 'yarn kbn bootstrap' or 'node scripts/register_git_hook' + +# pre-commit script takes zero arguments: https://git-scm.com/docs/githooks#_pre_commit + +set -euo pipefail + +# Make it possible to terminate pre commit hook +# using ctrl-c so nothing else would happen or be +# sent to the output. +# +# The correct exit code on that situation +# according the linux documentation project is 130 +# https://www.tldp.org/LDP/abs/html/exitcodes.html +trap "exit 130" INT + +has_node() { + command -v node >/dev/null 2>&1 +} + +has_nvm() { + command -v nvm >/dev/null 2>&1 +} + +try_load_node_from_nvm_paths () { + # If nvm is not loaded, load it + has_node || { + NVM_SH="${HOME_DIR}/.nvm/nvm.sh" + + if [ "${process.platform}" == "darwin" ] && [ -s "$(brew --prefix nvm)/nvm.sh" ]; then + NVM_SH="$(brew --prefix nvm)/nvm.sh" + fi + + export NVM_DIR="${HOME_DIR}/.nvm" + + [ -s "$NVM_SH" ] && \. "$NVM_SH" + + # If nvm has been loaded correctly, use project .nvmrc + has_nvm && nvm use + } +} + +extend_user_path() { + if [ "${process.platform}" == "win32" ]; then + export PATH="$PATH:/c/Program Files/nodejs" + else + export PATH="$PATH:/usr/local/bin:/usr/local" + try_load_node_from_nvm_paths + fi +} + +# Extend path with common path locations for node +# in order to make the hook working on git GUI apps +extend_user_path + +# Check if we have node js bin in path +has_node || { + echo "Can't found node bin in the PATH. Please update the PATH to proceed." + echo "If your PATH already has the node bin, maybe you are using some git GUI app." + echo "Can't found node bin in the PATH. Please update the PATH to proceed." + echo "If your PATH already has the node bin, maybe you are using some git GUI app not launched from the shell." + echo "In order to proceed, you need to config the PATH used by the application that are launching your git GUI app." + echo "If you are running macOS, you can do that using:" + echo "'sudo launchctl config user path /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'" + + exit 1 +} + +execute_precommit_hook() { + node scripts/precommit_hook || return 1 + + PRECOMMIT_FILE="./.git/hooks/pre-commit.local" + if [ -x "\${PRECOMMIT_FILE}" ]; then + echo "Executing local precommit hook found in \${PRECOMMIT_FILE}" + "$PRECOMMIT_FILE" || return 1 + fi +} + +execute_precommit_hook || { + echo "Pre-commit hook failed (add --no-verify to bypass)"; + echo ' For eslint failures you can try running \`node scripts/precommit_hook --fix\`'; + exit 1; +} + +exit 0 +`; diff --git a/packages/kbn-es/src/utils/native_realm.test.js b/packages/kbn-es/src/utils/native_realm.test.js index 99c7ed1623014..54732f7136fcc 100644 --- a/packages/kbn-es/src/utils/native_realm.test.js +++ b/packages/kbn-es/src/utils/native_realm.test.js @@ -109,7 +109,7 @@ describe('setPasswords', () => { mockClient.security.getUser.mockImplementation(() => ({ body: { - kibana: { + kibana_system: { metadata: { _reserved: true, }, @@ -138,7 +138,7 @@ describe('setPasswords', () => { })); await nativeRealm.setPasswords({ - 'password.kibana': 'bar', + 'password.kibana_system': 'bar', }); expect(mockClient.security.changePassword.mock.calls).toMatchInlineSnapshot(` @@ -149,7 +149,7 @@ Array [ "password": "bar", }, "refresh": "wait_for", - "username": "kibana", + "username": "kibana_system", }, ], Array [ @@ -188,7 +188,7 @@ describe('getReservedUsers', () => { it('returns array of reserved usernames', async () => { mockClient.security.getUser.mockImplementation(() => ({ body: { - kibana: { + kibana_system: { metadata: { _reserved: true, }, @@ -206,17 +206,17 @@ describe('getReservedUsers', () => { }, })); - expect(await nativeRealm.getReservedUsers()).toEqual(['kibana', 'logstash_system']); + expect(await nativeRealm.getReservedUsers()).toEqual(['kibana_system', 'logstash_system']); }); }); describe('setPassword', () => { it('sets password for provided user', async () => { - await nativeRealm.setPassword('kibana', 'foo'); + await nativeRealm.setPassword('kibana_system', 'foo'); expect(mockClient.security.changePassword).toHaveBeenCalledWith({ body: { password: 'foo' }, refresh: 'wait_for', - username: 'kibana', + username: 'kibana_system', }); }); @@ -226,7 +226,7 @@ describe('setPassword', () => { }); await expect( - nativeRealm.setPassword('kibana', 'foo') + nativeRealm.setPassword('kibana_system', 'foo') ).rejects.toThrowErrorMatchingInlineSnapshot(`"SomeError"`); }); }); diff --git a/packages/kbn-interpreter/tasks/build/server_code_transformer.js b/packages/kbn-interpreter/tasks/build/server_code_transformer.js deleted file mode 100644 index 4bd9220993c62..0000000000000 --- a/packages/kbn-interpreter/tasks/build/server_code_transformer.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { extname } = require('path'); - -const { transform } = require('@babel/core'); - -exports.createServerCodeTransformer = sourceMaps => { - return (content, path) => { - switch (extname(path)) { - case '.js': - const { code = '' } = transform(content.toString('utf8'), { - filename: path, - ast: false, - code: true, - sourceMaps: sourceMaps ? 'inline' : false, - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }); - - return code; - - default: - return content.toString('utf8'); - } - }; -}; diff --git a/packages/kbn-interpreter/tasks/build/server_code_transformer.test.js b/packages/kbn-interpreter/tasks/build/server_code_transformer.test.js deleted file mode 100644 index 519e529c20bf5..0000000000000 --- a/packages/kbn-interpreter/tasks/build/server_code_transformer.test.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { readFileSync } from 'fs'; -import { resolve } from 'path'; -import { createServerCodeTransformer } from './server_code_transformer'; - -const JS_FIXTURE_PATH = resolve(__dirname, '__fixtures__/sample.js'); -const JS_FIXTURE = readFileSync(JS_FIXTURE_PATH); - -describe('js support', () => { - it('transpiles js file', () => { - const transformer = createServerCodeTransformer(); - expect(transformer(JS_FIXTURE, JS_FIXTURE_PATH)).toMatchInlineSnapshot(` -"\\"use strict\\"; - -var _util = _interopRequireDefault(require(\\"util\\")); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* eslint-disable */ -console.log(_util.default.format('hello world'));" -`); - }); - - it('throws errors for js syntax errors', () => { - const transformer = createServerCodeTransformer(); - expect(() => transformer(Buffer.from(`export default 'foo`), JS_FIXTURE_PATH)).toThrowError( - /Unterminated string constant/ - ); - }); -}); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 5ea031595d1d4..47ed69bc95697 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -50,7 +50,7 @@ "html": "1.0.0", "html-loader": "^0.5.5", "imports-loader": "^0.8.0", - "jquery": "^3.4.1", + "jquery": "^3.5.0", "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index a2248f1ae655e..8259f251a9be3 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "19.1.2", - "@elastic/eui": "22.3.0", + "@elastic/charts": "19.2.0", + "@elastic/eui": "22.3.1", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", @@ -18,7 +18,7 @@ "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", - "jquery": "^3.4.1", + "jquery": "^3.5.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "monaco-editor": "~0.17.0", diff --git a/renovate.json5 b/renovate.json5 index 61b2485ecf44b..c4efa86366bf4 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -771,6 +771,14 @@ '@types/podium', ], }, + { + groupSlug: 'pretty-ms', + groupName: 'pretty-ms related packages', + packageNames: [ + 'pretty-ms', + '@types/pretty-ms', + ], + }, { groupSlug: 'proper-lockfile', groupName: 'proper-lockfile related packages', @@ -864,6 +872,14 @@ '@types/sinon', ], }, + { + groupSlug: 'stats-lite', + groupName: 'stats-lite related packages', + packageNames: [ + 'stats-lite', + '@types/stats-lite', + ], + }, { groupSlug: 'storybook', groupName: 'storybook related packages', diff --git a/scripts/kibana.js b/scripts/kibana.js index f5a63e6c07dd6..4da739469ffb1 100644 --- a/scripts/kibana.js +++ b/scripts/kibana.js @@ -17,6 +17,6 @@ * under the License. */ -require('../src/apm')(process.env.ELASTIC_APM_PROXY_SERVICE_NAME || 'kibana-proxy'); require('../src/setup_node_env'); +require('../src/apm')(process.env.ELASTIC_APM_PROXY_SERVICE_NAME || 'kibana-proxy'); require('../src/cli/cli'); diff --git a/scripts/register_git_hook.js b/scripts/register_git_hook.js index 8e03f17967f3f..af3f54619bcec 100644 --- a/scripts/register_git_hook.js +++ b/scripts/register_git_hook.js @@ -17,5 +17,5 @@ * under the License. */ -require('../src/setup_node_env'); -require('../src/dev/run_register_git_hook'); +require('../src/setup_node_env/prebuilt_dev_only_entry'); +require('@kbn/dev-utils/target/precommit_hook/cli'); diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 97dec3eead303..3b3e4d78320d2 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -268,7 +268,7 @@ export class ClusterManager { fromRoot('x-pack/plugins/siem/cypress'), fromRoot('x-pack/plugins/apm/e2e'), fromRoot('x-pack/plugins/apm/scripts'), - fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, + fromRoot('x-pack/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, 'plugins/java_languageserver', ]; diff --git a/src/cli/index.js b/src/cli/index.js index 45f88eaf82a5b..6dbdd800268a9 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -17,6 +17,6 @@ * under the License. */ -require('../apm')(); require('../setup_node_env'); +require('../apm')(); require('./cli'); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 29d0fe16ee126..471939121143a 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -79,7 +79,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { set('optimize.watch', true); if (!has('elasticsearch.username')) { - set('elasticsearch.username', 'kibana'); + set('elasticsearch.username', 'kibana_system'); } if (!has('elasticsearch.password')) { diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 80f12dd78214d..02d46b1583b59 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -990,6 +990,9 @@ ls -lh plugins/my_plugin/target/public/ you might see at least one js bundle - `my_plugin.plugin.js`. This is the only artifact loaded by the platform during bootstrap in the browser. The rule of thumb is to keep its size as small as possible. Other lazily loaded parts of your plugin present in the same folder as separate chunks under `{number}.plugin.js` names. If you want to investigate what your plugin bundle consists of you need to run `@kbn/optimizer` with `--profile` flag to get generated [webpack stats file](https://webpack.js.org/api/stats/). +```bash +node scripts/build_kibana_platform_plugins.js --dist --no-examples --profile +``` Many OSS tools are allowing you to analyze generated stats file - [an official tool](http://webpack.github.io/analyse/#modules) from webpack authors - [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 0dd77072e9eaf..8442f1ecc6411 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -26,6 +26,7 @@ import { InjectedMetadataSetup } from '../injected_metadata'; import { HttpSetup, HttpStart } from '../http'; import { OverlayStart } from '../overlays'; import { ContextSetup, IContextContainer } from '../context'; +import { PluginOpaqueId } from '../plugins'; import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; import { @@ -34,7 +35,6 @@ import { AppLeaveHandler, AppMount, AppMountDeprecated, - AppMounter, AppNavLinkStatus, AppStatus, AppUpdatableFields, @@ -145,6 +145,25 @@ export class ApplicationService { this.subscriptions.push(subscription); }; + const wrapMount = (plugin: PluginOpaqueId, app: App): AppMount => { + let handler: AppMount; + if (isAppMountDeprecated(app.mount)) { + handler = this.mountContext!.createHandler(plugin, app.mount); + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line no-console + console.warn( + `App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.` + ); + } + } else { + handler = app.mount; + } + return async params => { + this.currentAppId$.next(app.id); + return handler(params); + }; + }; + return { registerMountContext: this.mountContext!.registerContext, register: (plugin, app: App) => { @@ -162,24 +181,6 @@ export class ApplicationService { throw new Error('Cannot register an application route that includes HTTP base path'); } - let handler: AppMount; - - if (isAppMountDeprecated(app.mount)) { - handler = this.mountContext!.createHandler(plugin, app.mount); - // eslint-disable-next-line no-console - console.warn( - `App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.` - ); - } else { - handler = app.mount; - } - - const mount: AppMounter = async params => { - const unmount = await handler(params); - this.currentAppId$.next(app.id); - return unmount; - }; - const { updater$, ...appProps } = app; this.apps.set(app.id, { ...appProps, @@ -193,7 +194,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute: app.appRoute!, appBasePath: basePath.prepend(app.appRoute!), - mount, + mount: wrapMount(plugin, app), unmountBeforeMounting: false, }); }, diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index edf3583f384b8..60c36d3e330e0 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -17,6 +17,7 @@ * under the License. */ +import { take } from 'rxjs/operators'; import { createRenderer } from './utils'; import { createMemoryHistory, MemoryHistory } from 'history'; import { ApplicationService } from '../application_service'; @@ -56,6 +57,69 @@ describe('ApplicationService', () => { service = new ApplicationService(); }); + describe('navigating to apps', () => { + describe('using history.push', () => { + it('emits currentAppId$ before mounting the app', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise(resolve => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({}: AppMountParameters) => { + await promise; + return () => undefined; + }, + }); + + const { currentAppId$, getComponent } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await navigate('/app/app1'); + + expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); + + resolveMount!(); + + expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); + }); + }); + + describe('using navigateToApp', () => { + it('emits currentAppId$ before mounting the app', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise(resolve => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({}: AppMountParameters) => { + await promise; + return () => undefined; + }, + }); + + const { navigateToApp, currentAppId$ } = await service.start(startDeps); + + await navigateToApp('app1'); + + expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); + + resolveMount!(); + + expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1'); + }); + }); + }); + describe('leaving an application that registered an app leave handler', () => { it('navigates to the new app if action is default', async () => { startDeps.overlays.openConfirm.mockResolvedValue(true); diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 89007461b63e6..4a79dd8869c1c 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -23,7 +23,8 @@ import { ChromeBreadcrumb, ChromeService, InternalChromeStart, -} from './chrome_service'; + NavType, +} from './'; const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { @@ -72,6 +73,7 @@ const createStartContractMock = () => { setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), getIsNavDrawerLocked$: jest.fn(), + getNavType$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); @@ -81,6 +83,7 @@ const createStartContractMock = () => { startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); + startContract.getNavType$.mockReturnValue(new BehaviorSubject('modern' as NavType)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index bf531aaa00fac..327be61cc63e3 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -17,19 +17,18 @@ * under the License. */ -import * as Rx from 'rxjs'; -import { take, toArray } from 'rxjs/operators'; import { shallow } from 'enzyme'; import React from 'react'; - +import * as Rx from 'rxjs'; +import { take, toArray } from 'rxjs/operators'; +import { App } from '../application'; import { applicationServiceMock } from '../application/application_service.mock'; +import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; -import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; -import { ChromeService } from './chrome_service'; -import { App } from '../application'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; +import { ChromeService } from './chrome_service'; class FakeApp implements App { public title = `${this.id} App`; @@ -163,7 +162,7 @@ describe('start', () => { }); describe('visibility', () => { - it('updates/emits the visibility', async () => { + it('emits false when no application is mounted', async () => { const { chrome, service } = await start(); const promise = chrome .getIsVisible$() @@ -177,33 +176,37 @@ describe('start', () => { await expect(promise).resolves.toMatchInlineSnapshot(` Array [ - true, - true, false, - true, + false, + false, + false, ] `); }); - it('always emits false if embed query string is preset when set up', async () => { + it('emits false until manually overridden when in embed mode', async () => { window.history.pushState(undefined, '', '#/home?a=b&embed=true'); + const startDeps = defaultStartDeps([new FakeApp('alpha')]); + const { navigateToApp } = startDeps.application; + const { chrome, service } = await start({ startDeps }); - const { chrome, service } = await start(); const promise = chrome .getIsVisible$() .pipe(toArray()) .toPromise(); + await navigateToApp('alpha'); + chrome.setIsVisible(true); chrome.setIsVisible(false); - chrome.setIsVisible(true); + service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` Array [ false, false, - false, + true, false, ] `); @@ -228,7 +231,7 @@ describe('start', () => { await expect(promise).resolves.toMatchInlineSnapshot(` Array [ - true, + false, true, false, true, @@ -245,13 +248,13 @@ describe('start', () => { .pipe(toArray()) .toPromise(); - navigateToApp('alpha'); + await navigateToApp('alpha'); chrome.setIsVisible(true); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` Array [ - true, + false, false, false, ] diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 7c9b644b8b984..3fc22caaefb04 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -17,28 +17,26 @@ * under the License. */ +import { Breadcrumb as EuiBreadcrumb, IconType } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; -import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs'; +import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; - -import { i18n } from '@kbn/i18n'; -import { IconType, Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; - -import { InjectedMetadataStart } from '../injected_metadata'; -import { NotificationsStart } from '../notifications'; import { InternalApplicationStart } from '../application'; +import { DocLinksStart } from '../doc_links'; import { HttpStart } from '../http'; - +import { InjectedMetadataStart } from '../injected_metadata'; +import { NotificationsStart } from '../notifications'; +import { IUiSettingsClient } from '../ui_settings'; +import { KIBANA_ASK_ELASTIC_LINK } from './constants'; +import { ChromeDocTitle, DocTitleService } from './doc_title'; +import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; -import { NavControlsService, ChromeNavControls } from './nav_controls'; -import { DocTitleService, ChromeDocTitle } from './doc_title'; -import { LoadingIndicator, Header } from './ui'; -import { DocLinksStart } from '../doc_links'; +import { Header, LoadingIndicator } from './ui'; +import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { KIBANA_ASK_ELASTIC_LINK } from './constants'; -import { IUiSettingsClient } from '../ui_settings'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -91,8 +89,7 @@ interface StartDeps { /** @internal */ export class ChromeService { private isVisible$!: Observable; - private appHidden$!: Observable; - private toggleHidden$!: BehaviorSubject; + private isForceHidden$!: BehaviorSubject; private readonly stop$ = new ReplaySubject(1); private readonly navControls = new NavControlsService(); private readonly navLinks = new NavLinksService(); @@ -111,13 +108,12 @@ export class ChromeService { private initVisibility(application: StartDeps['application']) { // Start off the chrome service hidden if "embed" is in the hash query string. const isEmbedded = 'embed' in parse(location.hash.slice(1), true).query; + this.isForceHidden$ = new BehaviorSubject(isEmbedded); - this.toggleHidden$ = new BehaviorSubject(isEmbedded); - this.appHidden$ = merge( - // Default the app being hidden to the same value initial value as the chrome visibility - // in case the application service has not emitted an app ID yet, since we want to trigger - // combineLatest below regardless of having an application value yet. - of(isEmbedded), + const appHidden$ = merge( + // For the isVisible$ logic, having no mounted app is equivalent to having a hidden app + // in the sense that the chrome UI should not be displayed until a non-chromeless app is mounting or mounted + of(true), application.currentAppId$.pipe( flatMap(appId => application.applications$.pipe( @@ -128,8 +124,8 @@ export class ChromeService { ) ) ); - this.isVisible$ = combineLatest([this.appHidden$, this.toggleHidden$]).pipe( - map(([appHidden, toggleHidden]) => !(appHidden || toggleHidden)), + this.isVisible$ = combineLatest([appHidden$, this.isForceHidden$]).pipe( + map(([appHidden, forceHidden]) => !appHidden && !forceHidden), takeUntil(this.stop$) ); } @@ -165,6 +161,10 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + // TODO #64541 + // Can delete + const getNavType$ = uiSettings.get$('pageNavigation').pipe(takeUntil(this.stop$)); + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { @@ -202,6 +202,7 @@ export class ChromeService { navControlsRight$={navControls.getRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} + navType$={getNavType$} /> ), @@ -221,7 +222,7 @@ export class ChromeService { getIsVisible$: () => this.isVisible$, - setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible), + setIsVisible: (isVisible: boolean) => this.isForceHidden$.next(!isVisible), getApplicationClasses$: () => applicationClasses$.pipe( @@ -262,6 +263,8 @@ export class ChromeService { setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url), getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, + + getNavType$: () => getNavType$, }; } @@ -408,6 +411,13 @@ export interface ChromeStart { * Get an observable of the current locked state of the nav drawer. */ getIsNavDrawerLocked$(): Observable; + + /** + * Get the navigation type + * TODO #64541 + * Can delete + */ + getNavType$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index 4a500836990a7..cc1e0851f5944 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -33,6 +33,7 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './ui/header/header_help_menu'; +export { NavType } from './ui'; export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './nav_links'; export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; export { ChromeNavControl, ChromeNavControls } from './nav_controls'; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap new file mode 100644 index 0000000000000..14d5b2e8fdcbb --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -0,0 +1,4506 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapsibleNav renders links grouped by category 1`] = ` + + + + + + } + /> + + + + +
+ +
+
+
+ + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
+
+ +
+ + + + + +`; + +exports[`CollapsibleNav renders the default nav 1`] = ` + + + +`; + +exports[`CollapsibleNav renders the default nav 2`] = ` + + + + + + } + /> + + + + +
+ +
+
+
+ + + + +
+
+ +
+ + + + + +`; + +exports[`CollapsibleNav renders the default nav 3`] = ` + + + + + + +
+ +
+
+
+ + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
+
+ +
+ + + + + +`; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index f19728a52dd70..5c5e7f18b60a4 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,25 +1,12 @@ -@import '@elastic/eui/src/components/header/variables'; -@import '@elastic/eui/src/components/nav_drawer/variables'; - -.chrHeaderWrapper { +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) { width: 100%; position: fixed; top: 0; z-index: 10; } -.chrHeaderWrapper ~ .app-wrapper:not(.hidden-chrome) { - top: $euiHeaderChildSize; - left: $euiHeaderChildSize; - - // HOTFIX: Temporary fix for flyouts not inside portals - // SASSTODO: Find an actual solution - .euiFlyout { - top: $euiHeaderChildSize; - height: calc(100% - #{$euiHeaderChildSize}); - } -} - .chrHeaderHelpMenu__version { text-transform: none; } @@ -29,19 +16,8 @@ margin-right: $euiSize; } -// Mobile header is smaller -@include euiBreakpoint('xs', 's') { - .chrHeaderWrapper ~ .app-wrapper:not(.hidden-chrome) { - left: 0; - } -} - -@include euiBreakpoint('xl') { - .chrHeaderWrapper--navIsLocked { - ~ .app-wrapper:not(.hidden-chrome) { - // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) - left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important - transition: left $euiAnimSpeedFast $euiAnimSlightResistance; - } +.header__toggleNavButtonSection { + .euiBody--collapsibleNavIsDocked & { + display: none; } } diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx new file mode 100644 index 0000000000000..4a9d3071b93be --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; +import { CollapsibleNav } from './collapsible_nav'; +import { AppCategory } from '../../../../types'; +import { DEFAULT_APP_CATEGORIES } from '../../..'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; + +function mockLink(label: string, category?: AppCategory) { + return { + key: label, + label, + href: label, + isActive: true, + onClick: () => {}, + category, + 'data-test-subj': label, + }; +} + +function mockRecentNavLink(label: string) { + return { + href: label, + label, + title: label, + 'aria-label': label, + }; +} + +function mockProps() { + return { + id: 'collapsible-nav', + homeHref: '/', + isLocked: false, + isOpen: false, + navLinks: [], + recentNavLinks: [], + storage: new StubBrowserStorage(), + onIsOpenUpdate: () => {}, + onIsLockedUpdate: () => {}, + }; +} + +describe('CollapsibleNav', () => { + // this test is mostly an "EUI works as expected" sanity check + it('renders the default nav', () => { + const onLock = sinon.spy(); + const component = mount(); + expect(component).toMatchSnapshot(); + + component.setProps({ isOpen: true }); + expect(component).toMatchSnapshot(); + + component.setProps({ isLocked: true }); + expect(component).toMatchSnapshot(); + + // limit the find to buttons because jest also renders data-test-subj on a JSX wrapper element + component.find('button[data-test-subj="collapsible-nav-lock"]').simulate('click'); + expect(onLock.callCount).toEqual(1); + }); + + it('renders links grouped by category', () => { + // just a test of category functionality, categories are not accurate + const navLinks = [ + mockLink('discover', kibana), + mockLink('siem', security), + mockLink('metrics', observability), + mockLink('monitoring', management), + mockLink('visualize', kibana), + mockLink('dashboard', kibana), + mockLink('canvas'), // links should be able to be rendered top level as well + mockLink('logs', observability), + ]; + const recentNavLinks = [mockRecentNavLink('recent 1'), mockRecentNavLink('recent 2')]; + const component = mount( + + ); + expect(component).toMatchSnapshot(); + }); + + it('remembers collapsible section state', () => { + function expectNavLinksCount(component: ReactWrapper, count: number) { + expect( + component.find('.euiAccordion-isOpen a[data-test-subj="collapsibleNavAppLink"]').length + ).toEqual(count); + } + + const navLinks = [ + mockLink('discover', kibana), + mockLink('siem', security), + mockLink('metrics', observability), + mockLink('monitoring', management), + mockLink('visualize', kibana), + mockLink('dashboard', kibana), + mockLink('logs', observability), + ]; + const component = mount(); + expectNavLinksCount(component, 7); + component.find('[data-test-subj="collapsibleNavGroup-kibana"] button').simulate('click'); + expectNavLinksCount(component, 4); + component.setProps({ isOpen: false }); + expectNavLinksCount(component, 0); // double check the nav closed + component.setProps({ isOpen: true }); + expectNavLinksCount(component, 4); + }); +}); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx new file mode 100644 index 0000000000000..274195f1917a5 --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -0,0 +1,281 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiCollapsibleNav, + EuiCollapsibleNavGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiListGroup, + EuiListGroupItem, + EuiShowFor, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { groupBy, sortBy } from 'lodash'; +import React, { useRef } from 'react'; +import { AppCategory } from '../../../../types'; +import { OnIsLockedUpdate } from './'; +import { NavLink, RecentNavLink } from './nav_link'; + +function getAllCategories(allCategorizedLinks: Record) { + const allCategories = {} as Record; + + for (const [key, value] of Object.entries(allCategorizedLinks)) { + allCategories[key] = value[0].category; + } + + return allCategories; +} + +function getOrderedCategories( + mainCategories: Record, + categoryDictionary: ReturnType +) { + return sortBy( + Object.keys(mainCategories), + categoryName => categoryDictionary[categoryName]?.order + ); +} + +function getCategoryLocalStorageKey(id: string) { + return `core.navGroup.${id}`; +} + +function getIsCategoryOpen(id: string, storage: Storage) { + const value = storage.getItem(getCategoryLocalStorageKey(id)) ?? 'true'; + + return value === 'true'; +} + +function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { + storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`); +} + +interface Props { + isLocked: boolean; + isOpen: boolean; + navLinks: NavLink[]; + recentNavLinks: RecentNavLink[]; + homeHref: string; + id: string; + storage?: Storage; + onIsLockedUpdate: OnIsLockedUpdate; + onIsOpenUpdate: (isOpen?: boolean) => void; +} + +export function CollapsibleNav({ + isLocked, + isOpen, + navLinks, + recentNavLinks, + onIsLockedUpdate, + onIsOpenUpdate, + homeHref, + id, + storage = window.localStorage, +}: Props) { + const lockRef = useRef(null); + const groupedNavLinks = groupBy(navLinks, link => link?.category?.id); + const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; + const categoryDictionary = getAllCategories(allCategorizedLinks); + const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + + return ( + + {/* Pinned items */} + + + onIsOpenUpdate(false), + }, + ]} + maxWidth="none" + color="text" + gutterSize="none" + size="s" + /> + + + + + + + {/* Recently viewed */} + setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} + > + {recentNavLinks.length > 0 ? ( + { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, ...linkWithoutIcon } = link; + return linkWithoutIcon; + })} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + /> + ) : ( + +

+ {i18n.translate('core.ui.EmptyRecentlyViewed', { + defaultMessage: 'No recently viewed items', + })} +

+
+ )} +
+ + {/* Kibana, Observability, Security, and Management sections */} + {orderedCategories.map((categoryName, i) => { + const category = categoryDictionary[categoryName]!; + const links = allCategorizedLinks[categoryName].map( + ({ label, href, isActive, isDisabled, onClick }: NavLink) => ({ + label, + href, + isActive, + isDisabled, + 'data-test-subj': 'collapsibleNavAppLink', + onClick: (e: React.MouseEvent) => { + onIsOpenUpdate(false); + onClick(e); + }, + }) + ); + + return ( + setIsCategoryOpen(category.id, isCategoryOpen, storage)} + data-test-subj={`collapsibleNavGroup-${category.id}`} + > + + + ); + })} + + {/* Things with no category (largely for custom plugins) */} + {unknowns.map(({ label, href, icon, isActive, isDisabled, onClick }, i) => ( + + + ) => { + onIsOpenUpdate(false); + onClick(e); + }} + /> + + + ))} + + {/* Docking button only for larger screens that can support it*/} + + + + { + onIsLockedUpdate(!isLocked); + if (lockRef.current) { + lockRef.current.focus(); + } + }} + iconType={isLocked ? 'lock' : 'lockOpen'} + /> + + + +
+
+ ); +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 66b34c3db7bad..fb94ef46cdc2c 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -25,8 +25,8 @@ import { EuiIcon, // @ts-ignore EuiNavDrawer, - // @ts-ignore EuiShowFor, + htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Component, createRef } from 'react'; @@ -43,13 +43,14 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { OnIsLockedUpdate } from './'; +import { NavType, OnIsLockedUpdate } from './'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; -import { euiNavLink } from './nav_link'; +import { createNavLink, createRecentNavLink } from './nav_link'; import { HeaderLogo } from './header_logo'; import { NavDrawer } from './nav_drawer'; +import { CollapsibleNav } from './collapsible_nav'; export interface HeaderProps { kibanaVersion: string; @@ -70,6 +71,7 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked$: Rx.Observable; + navType$: Rx.Observable; onIsLockedUpdate: OnIsLockedUpdate; } @@ -83,11 +85,14 @@ interface State { navControlsRight: readonly ChromeNavControl[]; currentAppId: string | undefined; isLocked: boolean; + navType: NavType; + isOpen: boolean; } export class Header extends Component { private subscription?: Rx.Subscription; private navDrawerRef = createRef(); + private toggleCollapsibleNavRef = createRef(); constructor(props: HeaderProps) { super(props); @@ -105,6 +110,8 @@ export class Header extends Component { navControlsRight: [], currentAppId: '', isLocked, + navType: 'modern', + isOpen: false, }; } @@ -120,7 +127,8 @@ export class Header extends Component { this.props.navControlsLeft$, this.props.navControlsRight$, this.props.application.currentAppId$, - this.props.isLocked$ + this.props.isLocked$, + this.props.navType$ ) ).subscribe({ next: ([ @@ -129,7 +137,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, isLocked], + [navControlsLeft, navControlsRight, currentAppId, isLocked, navType], ]) => { this.setState({ appTitle, @@ -141,6 +149,7 @@ export class Header extends Component { navControlsRight, currentAppId, isLocked, + navType, }); }, }); @@ -176,7 +185,7 @@ export class Header extends Component { kibanaVersion, } = this.props; const navLinks = this.state.navLinks.map(link => - euiNavLink( + createNavLink( link, this.props.legacyMode, this.state.currentAppId, @@ -184,26 +193,54 @@ export class Header extends Component { this.props.application.navigateToApp ) ); + const recentNavLinks = this.state.recentlyAccessed.map(link => + createRecentNavLink(link, this.state.navLinks, this.props.basePath) + ); if (!isVisible) { return null; } const className = classnames( - 'chrHeaderWrapper', + 'chrHeaderWrapper', // TODO #64541 - delete this + 'hide-for-sharing', { 'chrHeaderWrapper--navIsLocked': this.state.isLocked, - }, - 'hide-for-sharing' + headerWrapper: this.state.navType === 'modern', + } ); - + const navId = htmlIdGenerator()(); return (
- + - - {this.renderMenuTrigger()} - + {this.state.navType === 'modern' ? ( + + { + this.setState({ isOpen: !this.state.isOpen }); + }} + aria-expanded={this.state.isOpen} + aria-pressed={this.state.isOpen} + aria-controls={navId} + ref={this.toggleCollapsibleNavRef} + > + + + + ) : ( + // TODO #64541 + // Delete this block + + + {this.renderMenuTrigger()} + + + )} { - + {this.state.navType === 'modern' ? ( + { + this.setState({ isOpen }); + if (this.toggleCollapsibleNavRef.current) { + this.toggleCollapsibleNavRef.current.focus(); + } + }} + /> + ) : ( + // TODO #64541 + // Delete this block + + )}
); } diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 793b8646dabf7..960ec637178e1 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -93,7 +93,7 @@ export function HeaderLogo({ href, forceNavigation, navLinks }: Props) { return ( onClick(e, forceNavigation, navLinks)} href={href} aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 49e002a66d939..a492273a65ba8 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,6 +18,7 @@ */ export { Header, HeaderProps } from './header'; +export { OnIsLockedUpdate, NavType } from './types'; export { ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, @@ -25,4 +26,3 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './header_help_menu'; -export type OnIsLockedUpdate = (isLocked: boolean) => void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index c57faec1e428d..7faee8edea43b 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -22,22 +22,18 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; import { OnIsLockedUpdate } from './'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { HttpStart } from '../../../http'; -import { NavLink } from './nav_link'; +import { NavLink, RecentNavLink } from './nav_link'; import { RecentLinks } from './recent_links'; export interface Props { isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; - chromeNavLinks: ChromeNavLink[]; - recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; - basePath: HttpStart['basePath']; + recentNavLinks: RecentNavLink[]; } function navDrawerRenderer( - { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, + { isLocked, onIsLockedUpdate, navLinks, recentNavLinks }: Props, ref: React.Ref ) { return ( @@ -50,11 +46,7 @@ function navDrawerRenderer( defaultMessage: 'Primary', })} > - {RecentLinks({ - recentlyAccessedItems, - navLinks: chromeNavLinks, - basePath, - })} + {RecentLinks({ recentNavLinks })} ) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); } @@ -30,15 +32,36 @@ function LinkIcon({ url }: { url: string }) { return ; } -export type NavLink = ReturnType; +export interface NavLink { + key: string; + label: string; + href: string; + isActive: boolean; + onClick(event: React.MouseEvent): void; + category?: AppCategory; + isDisabled?: boolean; + iconType?: string; + icon?: JSX.Element; + order?: number; + 'data-test-subj': string; +} -export function euiNavLink( +/** + * Create a link that's actually ready to be passed into EUI + * + * @param navLink + * @param legacyMode + * @param currentAppId + * @param basePath + * @param navigateToApp + */ +export function createNavLink( navLink: ChromeNavLink, legacyMode: boolean, currentAppId: string | undefined, basePath: HttpStart['basePath'], navigateToApp: CoreStart['application']['navigateToApp'] -) { +): NavLink { const { legacy, url, @@ -64,7 +87,7 @@ export function euiNavLink( key: id, label: tooltip ?? title, href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link - onClick(event: MouseEvent) { + onClick(event) { if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps @@ -85,3 +108,76 @@ export function euiNavLink( 'data-test-subj': 'navDrawerAppsMenuLink', }; } + +// Providing a buffer between the limit and the cut off index +// protects from truncating just the last couple (6) characters +const TRUNCATE_LIMIT: number = 64; +const TRUNCATE_AT: number = 58; + +function truncateRecentItemLabel(label: string): string { + if (label.length > TRUNCATE_LIMIT) { + label = `${label.substring(0, TRUNCATE_AT)}…`; + } + + return label; +} + +/** + * @param {string} url - a relative or root relative url. If a relative path is given then the + * absolute url returned will depend on the current page where this function is called from. For example + * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get + * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that + * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". + * @return {string} the relative url transformed into an absolute url + */ +function relativeToAbsolute(url: string) { + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} + +export interface RecentNavLink { + href: string; + label: string; + title: string; + 'aria-label': string; + iconType?: string; +} + +/** + * Add saved object type info to recently links + * + * Recent nav links are similar to normal nav links but are missing some Kibana Platform magic and + * because of legacy reasons have slightly different properties. + * @param recentLink + * @param navLinks + * @param basePath + */ +export function createRecentNavLink( + recentLink: ChromeRecentlyAccessedHistoryItem, + navLinks: ChromeNavLink[], + basePath: HttpStart['basePath'] +) { + const { link, label } = recentLink; + const href = relativeToAbsolute(basePath.prepend(link)); + const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl ?? nl.subUrlBase)); + let titleAndAriaLabel = label; + + if (navLink) { + titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', { + defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}', + values: { + recentlyAccessedItemLinklabel: label, + pageType: navLink.title, + }, + }); + } + + return { + href, + label: truncateRecentItemLabel(label), + title: titleAndAriaLabel, + 'aria-label': titleAndAriaLabel, + iconType: navLink?.euiIconType, + }; +} diff --git a/src/core/public/chrome/ui/header/recent_links.tsx b/src/core/public/chrome/ui/header/recent_links.tsx index 57cb1d9541bcd..019cdce0b43c6 100644 --- a/src/core/public/chrome/ui/header/recent_links.tsx +++ b/src/core/public/chrome/ui/header/recent_links.tsx @@ -21,73 +21,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawerGroup } from '@elastic/eui'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { HttpStart } from '../../../http'; - -// Providing a buffer between the limit and the cut off index -// protects from truncating just the last couple (6) characters -const TRUNCATE_LIMIT: number = 64; -const TRUNCATE_AT: number = 58; - -export function truncateRecentItemLabel(label: string): string { - if (label.length > TRUNCATE_LIMIT) { - label = `${label.substring(0, TRUNCATE_AT)}…`; - } - - return label; -} - -/** - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -function relativeToAbsolute(url: string) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - -function prepareForEUI( - recentlyAccessed: ChromeRecentlyAccessedHistoryItem[], - navLinks: ChromeNavLink[], - basePath: HttpStart['basePath'] -) { - return recentlyAccessed.map(({ link, label }) => { - const href = relativeToAbsolute(basePath.prepend(link)); - const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl ?? nl.subUrlBase)); - let titleAndAriaLabel = label; - - if (navLink) { - titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', { - defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}', - values: { - recentlyAccessedItemLinklabel: label, - pageType: navLink.title, - }, - }); - } - - return { - href, - label: truncateRecentItemLabel(label), - title: titleAndAriaLabel, - 'aria-label': titleAndAriaLabel, - iconType: navLink?.euiIconType, - }; - }); -} +import { RecentNavLink } from './nav_link'; interface Props { - recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; - navLinks: ChromeNavLink[]; - basePath: HttpStart['basePath']; + recentNavLinks: RecentNavLink[]; } -export function RecentLinks({ recentlyAccessedItems, navLinks, basePath }: Props) { +export function RecentLinks({ recentNavLinks }: Props) { return ( void; +export type NavType = 'modern' | 'legacy'; diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 460e19b7d9780..4f6ad90cb96a3 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -25,4 +25,5 @@ export { ChromeHelpExtensionMenuDiscussLink, ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, + NavType, } from './header'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index b4f64125a03ef..3b2d9ed3c0b02 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -54,6 +54,7 @@ import { ChromeStart, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, + NavType, } from './chrome'; import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; @@ -77,7 +78,17 @@ import { } from './context'; export { CoreContext, CoreSystem } from './core_system'; -export { RecursiveReadonly, DEFAULT_APP_CATEGORIES } from '../utils'; +export { + RecursiveReadonly, + DEFAULT_APP_CATEGORIES, + getFlattenedObject, + URLMeaningfulParts, + modifyUrl, + isRelativeUrl, + Freezable, + deepFreeze, + assertNever, +} from '../utils'; export { AppCategory, UiSettingsParams, @@ -344,4 +355,5 @@ export { PluginOpaqueId, IUiSettingsClient, UiSettingsState, + NavType, }; diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 6b4b22b8541bc..fa83b34e06b81 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -13,12 +13,7 @@ Array [ Array [
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ Array [
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index af06b207889c2..225ef611c0298 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -16,6 +16,7 @@ import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; import { Observable } from 'rxjs'; +import { ParsedQuery } from 'query-string'; import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; import React from 'react'; import * as Rx from 'rxjs'; @@ -54,6 +55,7 @@ export interface AppBase { export interface AppCategory { ariaLabel?: string; euiIconType?: string; + id: string; label: string; order?: number; } @@ -174,6 +176,9 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public +export function assertNever(x: never): never; + // @public export interface Capabilities { [key: string]: Record>; @@ -339,6 +344,7 @@ export interface ChromeStart { getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; + getNavType$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; recentlyAccessed: ChromeRecentlyAccessed; @@ -434,25 +440,33 @@ export class CoreSystem { stop(): void; } +// @public +export function deepFreeze(object: T): RecursiveReadonly; + // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Readonly<{ - analyze: { + kibana: { + id: string; label: string; + euiIconType: string; order: number; }; observability: { + id: string; label: string; euiIconType: string; order: number; }; security: { + id: string; label: string; order: number; euiIconType: string; }; management: { + id: string; label: string; - euiIconType: string; + order: number; }; }>; @@ -584,6 +598,16 @@ export interface FatalErrorsSetup { // @public export type FatalErrorsStart = FatalErrorsSetup; +// @public (undocumented) +export type Freezable = { + [k: string]: any; +} | any[]; + +// @public +export function getFlattenedObject(rootValue: Record): { + [key: string]: any; +}; + // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; @@ -795,6 +819,9 @@ export interface ImageValidation { }; } +// @public +export function isRelativeUrl(candidatePath: string): boolean; + // @public export type IToasts = Pick; @@ -857,9 +884,17 @@ export interface LegacyNavLink { url: string; } +// @public +export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; + // @public export type MountPoint = (element: T) => UnmountCallback; +// Warning: (ae-missing-release-tag) "NavType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type NavType = 'modern' | 'legacy'; + // @public (undocumented) export interface NotificationsSetup { // (undocumented) @@ -1356,6 +1391,26 @@ export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'sel // @public export type UnmountCallback = () => void; +// @public +export interface URLMeaningfulParts { + // (undocumented) + auth?: string | null; + // (undocumented) + hash?: string | null; + // (undocumented) + hostname?: string | null; + // (undocumented) + pathname?: string | null; + // (undocumented) + port?: string | null; + // (undocumented) + protocol?: string | null; + // (undocumented) + query: ParsedQuery; + // (undocumented) + slashes?: boolean | null; +} + // @public export interface UserProvidedValues { // (undocumented) diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index ff28fc75e367d..8032bc458822f 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,3 +1,6 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + /** * stretch the root element of the Kibana application to set the base-size that * flexed children should keep. Only works when paired with root styles applied @@ -9,7 +12,9 @@ min-height: 100%; } -.app-wrapper { +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) ~ .app-wrapper { display: flex; flex-flow: column nowrap; position: absolute; @@ -20,6 +25,22 @@ z-index: 5; margin: 0 auto; + &:not(.hidden-chrome) { + top: $euiHeaderChildSize; + left: $euiHeaderChildSize; + + // HOTFIX: Temporary fix for flyouts not inside portals + // SASSTODO: Find an actual solution + .euiFlyout { + top: $euiHeaderChildSize; + height: calc(100% - #{$euiHeaderChildSize}); + } + + @include euiBreakpoint('xs', 's') { + left: 0; + } + } + /** * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state * when we're looking at the log-in screen. @@ -33,6 +54,32 @@ } } +// TODO #64541 +// Delete this block +@include euiBreakpoint('xl') { + .chrHeaderWrapper--navIsLocked:not(.headerWrapper) { + ~ .app-wrapper:not(.hidden-chrome) { + // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) + left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important + transition: left $euiAnimSpeedFast $euiAnimSlightResistance; + } + } +} + +// TODO #64541 +// Remove .headerWrapper and header conditionals +.headerWrapper ~ .app-wrapper, +:not(header) ~ .app-wrapper { + display: flex; + flex-flow: column nowrap; + margin: 0 auto; + min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + &.hidden-chrome { + min-height: 100vh; + } +} + .app-wrapper-panel { display: flex; flex-grow: 1; diff --git a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png index 54b274dbc8eb1..18a86e5b95c46 100644 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png and b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png differ diff --git a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png b/src/core/server/core_app/assets/favicons/android-chrome-256x256.png index 4fb79e35a8fbd..8238d772ce40b 100644 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-256x256.png and b/src/core/server/core_app/assets/favicons/android-chrome-256x256.png differ diff --git a/src/core/server/core_app/assets/favicons/android-chrome-512x512.png b/src/core/server/core_app/assets/favicons/android-chrome-512x512.png deleted file mode 100644 index 5095b839b77a2..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-512x512.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/apple-touch-icon.png b/src/core/server/core_app/assets/favicons/apple-touch-icon.png index 11a714394b172..1ffeb0852a170 100644 Binary files a/src/core/server/core_app/assets/favicons/apple-touch-icon.png and b/src/core/server/core_app/assets/favicons/apple-touch-icon.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon-16x16.png b/src/core/server/core_app/assets/favicons/favicon-16x16.png index 1ff8f0caa2dfc..631f5b7c7d74b 100644 Binary files a/src/core/server/core_app/assets/favicons/favicon-16x16.png and b/src/core/server/core_app/assets/favicons/favicon-16x16.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon-32x32.png b/src/core/server/core_app/assets/favicons/favicon-32x32.png index 709b651e15eba..bf94dfa995f37 100644 Binary files a/src/core/server/core_app/assets/favicons/favicon-32x32.png and b/src/core/server/core_app/assets/favicons/favicon-32x32.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon.ico b/src/core/server/core_app/assets/favicons/favicon.ico index 9d0ed69fb63e4..db30798a6cf32 100644 Binary files a/src/core/server/core_app/assets/favicons/favicon.ico and b/src/core/server/core_app/assets/favicons/favicon.ico differ diff --git a/src/core/server/core_app/assets/favicons/manifest.json b/src/core/server/core_app/assets/favicons/manifest.json index 17b3c4b2d9e52..de65106f489b7 100644 --- a/src/core/server/core_app/assets/favicons/manifest.json +++ b/src/core/server/core_app/assets/favicons/manifest.json @@ -1,5 +1,6 @@ { "name": "", + "short_name": "", "icons": [ { "src": "/android-chrome-192x192.png", diff --git a/src/core/server/core_app/assets/favicons/mstile-144x144.png b/src/core/server/core_app/assets/favicons/mstile-144x144.png deleted file mode 100644 index be839dad41365..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-144x144.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/mstile-150x150.png b/src/core/server/core_app/assets/favicons/mstile-150x150.png index 0a2078511231b..82769c1ef242b 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-150x150.png and b/src/core/server/core_app/assets/favicons/mstile-150x150.png differ diff --git a/src/core/server/core_app/assets/favicons/mstile-310x150.png b/src/core/server/core_app/assets/favicons/mstile-310x150.png deleted file mode 100644 index 8c4d4ec7af840..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-310x150.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/mstile-310x310.png b/src/core/server/core_app/assets/favicons/mstile-310x310.png deleted file mode 100644 index 82701d9bb35da..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-310x310.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/mstile-70x70.png b/src/core/server/core_app/assets/favicons/mstile-70x70.png deleted file mode 100644 index 794a22ab1ee6f..0000000000000 Binary files a/src/core/server/core_app/assets/favicons/mstile-70x70.png and /dev/null differ diff --git a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg index 839ee14d59444..38a64142be0b7 100644 --- a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg +++ b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg @@ -2,34 +2,33 @@ Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - + + + + + + diff --git a/src/core/server/elasticsearch/__snapshots__/elasticsearch_config.test.ts.snap b/src/core/server/elasticsearch/__snapshots__/elasticsearch_config.test.ts.snap index e81336c8863f5..75627f311d9a5 100644 --- a/src/core/server/elasticsearch/__snapshots__/elasticsearch_config.test.ts.snap +++ b/src/core/server/elasticsearch/__snapshots__/elasticsearch_config.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#username throws if equal to "elastic", only while running from source 1`] = `"[username]: value of \\"elastic\\" is forbidden. This is a superuser account that can obfuscate privilege-related issues. You should use the \\"kibana\\" user instead."`; +exports[`#username throws if equal to "elastic", only while running from source 1`] = `"[username]: value of \\"elastic\\" is forbidden. This is a superuser account that can obfuscate privilege-related issues. You should use the \\"kibana_system\\" user instead."`; diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index de3f57298f461..cb4501a51e849 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -315,12 +315,21 @@ describe('deprecations', () => { const { messages } = applyElasticsearchDeprecations({ username: 'elastic' }); expect(messages).toMatchInlineSnapshot(` Array [ - "Setting [${CONFIG_PATH}.username] to \\"elastic\\" is deprecated. You should use the \\"kibana\\" user instead.", + "Setting [${CONFIG_PATH}.username] to \\"elastic\\" is deprecated. You should use the \\"kibana_system\\" user instead.", ] `); }); - it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => { + it('logs a warning if elasticsearch.username is set to "kibana"', () => { + const { messages } = applyElasticsearchDeprecations({ username: 'kibana' }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting [${CONFIG_PATH}.username] to \\"kibana\\" is deprecated. You should use the \\"kibana_system\\" user instead.", + ] + `); + }); + + it('does not log a warning if elasticsearch.username is set to something besides "elastic" or "kibana"', () => { const { messages } = applyElasticsearchDeprecations({ username: 'otheruser' }); expect(messages).toHaveLength(0); }); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index d3012e361b3ed..c87c94bcd0b6a 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -55,7 +55,7 @@ export const configSchema = schema.object({ if (rawConfig === 'elastic') { return ( 'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' + - 'privilege-related issues. You should use the "kibana" user instead.' + 'privilege-related issues. You should use the "kibana_system" user instead.' ); } }, @@ -131,7 +131,11 @@ const deprecations: ConfigDeprecationProvider = () => [ } if (es.username === 'elastic') { log( - `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana" user instead.` + `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.` + ); + } else if (es.username === 'kibana') { + log( + `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.` ); } if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 86192245bd2d1..cf999875b18f8 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -288,7 +288,17 @@ export { MetricsServiceSetup, } from './metrics'; -export { RecursiveReadonly } from '../utils'; +export { + RecursiveReadonly, + DEFAULT_APP_CATEGORIES, + getFlattenedObject, + URLMeaningfulParts, + modifyUrl, + isRelativeUrl, + Freezable, + deepFreeze, + assertNever, +} from '../utils'; export { SavedObject, diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts index 44d080ec37a25..5e84f27acabd5 100644 --- a/src/core/server/legacy/plugins/get_nav_links.test.ts +++ b/src/core/server/legacy/plugins/get_nav_links.test.ts @@ -133,6 +133,7 @@ describe('getNavLinks', () => { id: 'app-a', title: 'AppA', category: { + id: 'foo', label: 'My Category', }, order: 42, @@ -151,6 +152,7 @@ describe('getNavLinks', () => { id: 'app-a', title: 'AppA', category: { + id: 'foo', label: 'My Category', }, order: 42, @@ -211,6 +213,7 @@ describe('getNavLinks', () => { id: 'link-a', title: 'AppA', category: { + id: 'foo', label: 'My Second Cat', }, order: 72, @@ -232,6 +235,7 @@ describe('getNavLinks', () => { id: 'link-a', title: 'AppA', category: { + id: 'foo', label: 'My Second Cat', }, order: 72, diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts index dd5c256cf1600..3b982a06cf06c 100644 --- a/src/core/server/metrics/integration_tests/server_collector.test.ts +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -185,18 +185,22 @@ describe('ServerMetricsCollector', () => { let metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); - sendGet('/').end(() => null); + // supertest requests are executed when calling `.then` (or awaiting them). + // however in this test we need to send the request now and await for it later in the code. + // also using `.end` is not possible as it would execute the request twice. + // so the only option is this noop `.then`. + const res1 = sendGet('/').then(res => res); await waitForHits(1); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(1); - sendGet('/').end(() => null); + const res2 = sendGet('/').then(res => res); await waitForHits(2); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(2); waitSubject.next('go'); - await delay(requestWaitDelay); + await Promise.all([res1, res2]); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); }); diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index 73e119a5a97e7..76af229ac02ba 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -74,7 +74,7 @@ export const Template: FunctionComponent = ({ - Elastic Kibana + Elastic {/* Favicons (generated from http://realfavicongenerator.net/) */} = ({ className="kbnWelcomeText" data-error-message={i18n('core.ui.welcomeErrorMessage', { defaultMessage: - 'Elastic Kibana did not load properly. Check the server output for more information.', + 'Elastic did not load properly. Check the server output for more information.', })} > - {i18n('core.ui.welcomeMessage', { defaultMessage: 'Loading Elastic Kibana' })} + {i18n('core.ui.welcomeMessage', { defaultMessage: 'Loading Elastic' })}
@@ -146,7 +146,7 @@ export const Template: FunctionComponent = ({
{i18n('core.ui.legacyBrowserMessage', { defaultMessage: - 'This Kibana installation has strict security requirements enabled that your current browser does not meet.', + 'This Elastic installation has strict security requirements enabled that your current browser does not meet.', })}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e8b77a8570291..62d11ee7cf9a7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -103,6 +103,7 @@ import { NodesInfoParams } from 'elasticsearch'; import { NodesStatsParams } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; +import { ParsedQuery } from 'query-string'; import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; import { PutScriptParams } from 'elasticsearch'; @@ -388,6 +389,9 @@ export interface APICaller { (endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } +// @public +export function assertNever(x: never): never; + // @public (undocumented) export interface AssistanceAPIResponse { // (undocumented) @@ -691,6 +695,36 @@ export interface CustomHttpResponseOptions(object: T): RecursiveReadonly; + +// @internal (undocumented) +export const DEFAULT_APP_CATEGORIES: Readonly<{ + kibana: { + id: string; + label: string; + euiIconType: string; + order: number; + }; + observability: { + id: string; + label: string; + euiIconType: string; + order: number; + }; + security: { + id: string; + label: string; + order: number; + euiIconType: string; + }; + management: { + id: string; + label: string; + order: number; + }; +}>; + // @public (undocumented) export interface DeprecationAPIClientParams extends GenericParams { // (undocumented) @@ -838,6 +872,11 @@ export interface FakeRequest { headers: Headers; } +// @public (undocumented) +export type Freezable = { + [k: string]: any; +} | any[]; + // @public export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; @@ -847,6 +886,11 @@ export type GetAuthState = (request: KibanaRequest | LegacyRequest) state: T; }; +// @public +export function getFlattenedObject(rootValue: Record): { + [key: string]: any; +}; + // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; @@ -1034,6 +1078,9 @@ export type ISavedObjectTypeRegistry = Omit; +// @public +export function isRelativeUrl(candidatePath: string): boolean; + // @public export interface IUiSettingsClient { get: (key: string) => Promise; @@ -1289,6 +1336,9 @@ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; // @public (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; +// @public +export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; + // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; @@ -2447,6 +2497,26 @@ export interface UiSettingsServiceStart { // @public export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; +// @public +export interface URLMeaningfulParts { + // (undocumented) + auth?: string | null; + // (undocumented) + hash?: string | null; + // (undocumented) + hostname?: string | null; + // (undocumented) + pathname?: string | null; + // (undocumented) + port?: string | null; + // (undocumented) + protocol?: string | null; + // (undocumented) + query: ParsedQuery; + // (undocumented) + slashes?: boolean | null; +} + // @public export interface UserProvidedValues { // (undocumented) diff --git a/src/core/types/app_category.ts b/src/core/types/app_category.ts index 83a3693f009b6..8b39889b43a82 100644 --- a/src/core/types/app_category.ts +++ b/src/core/types/app_category.ts @@ -24,6 +24,11 @@ * @public */ export interface AppCategory { + /** + * Unique identifier for the categories + */ + id: string; + /** * Label used for cateogry name. * Also used as aria-label if one isn't set. diff --git a/src/core/utils/assert_never.ts b/src/core/utils/assert_never.ts index 8e47f07a02a87..c713b373493c5 100644 --- a/src/core/utils/assert_never.ts +++ b/src/core/utils/assert_never.ts @@ -17,8 +17,12 @@ * under the License. */ -// Can be used in switch statements to ensure we perform exhaustive checks, see -// https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking +/** + * Can be used in switch statements to ensure we perform exhaustive checks, see + * https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking + * + * @public + */ export function assertNever(x: never): never { throw new Error(`Unexpected object: ${x}`); } diff --git a/src/core/utils/deep_freeze.ts b/src/core/utils/deep_freeze.ts index 8c3f8f2258b61..b0f283c60d0fc 100644 --- a/src/core/utils/deep_freeze.ts +++ b/src/core/utils/deep_freeze.ts @@ -17,8 +17,6 @@ * under the License. */ -type Freezable = { [k: string]: any } | any[]; - // if we define this inside RecursiveReadonly TypeScript complains // eslint-disable-next-line @typescript-eslint/no-empty-interface interface RecursiveReadonlyArray extends Array> {} @@ -32,6 +30,15 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? Readonly<{ [K in keyof T]: RecursiveReadonly }> : T; +/** @public */ +export type Freezable = { [k: string]: any } | any[]; + +/** + * Apply Object.freeze to a value recursively and convert the return type to + * Readonly variant recursively + * + * @public + */ export function deepFreeze(object: T) { // for any properties that reference an object, makes sure that object is // recursively frozen as well diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 2285bd6afd365..5708bcfeac31a 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -21,13 +21,16 @@ import { i18n } from '@kbn/i18n'; /** @internal */ export const DEFAULT_APP_CATEGORIES = Object.freeze({ - analyze: { - label: i18n.translate('core.ui.analyzeNavList.label', { - defaultMessage: 'Analyze', + kibana: { + id: 'kibana', + label: i18n.translate('core.ui.kibanaNavList.label', { + defaultMessage: 'Kibana', }), + euiIconType: 'logoKibana', order: 1000, }, observability: { + id: 'observability', label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), @@ -35,6 +38,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ order: 2000, }, security: { + id: 'security', label: i18n.translate('core.ui.securityNavList.label', { defaultMessage: 'Security', }), @@ -42,9 +46,10 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ euiIconType: 'logoSecurity', }, management: { + id: 'management', label: i18n.translate('core.ui.managementNavList.label', { defaultMessage: 'Management', }), - euiIconType: 'managementApp', + order: 5000, }, }); diff --git a/src/core/utils/get_flattened_object.ts b/src/core/utils/get_flattened_object.ts index ce03793284236..25ca0c7c83e26 100644 --- a/src/core/utils/get_flattened_object.ts +++ b/src/core/utils/get_flattened_object.ts @@ -30,8 +30,7 @@ function shouldReadKeys(value: unknown): value is Record { * getFlattenedObject({ a: { b: 1, c: [2,3] } }) * // => { 'a.b': 1, 'a.c': [2,3] } * - * @param {Object} rootValue - * @returns {Object} + * @public */ export function getFlattenedObject(rootValue: Record) { if (!shouldReadKeys(rootValue)) { diff --git a/src/core/utils/url.ts b/src/core/utils/url.ts index c2bf80ce3f86f..910fc8eaa4381 100644 --- a/src/core/utils/url.ts +++ b/src/core/utils/url.ts @@ -23,6 +23,8 @@ import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; * We define our own typings because the current version of @types/node * declares properties to be optional "hostname?: string". * Although, parse call returns "hostname: null | string". + * + * @public */ export interface URLMeaningfulParts { auth?: string | null; @@ -63,6 +65,7 @@ export interface URLMeaningfulParts { * @param url The string url to parse. * @param urlModifier A function that will modify the parsed url, or return a new one. * @returns The modified and reformatted url + * @public */ export function modifyUrl( url: string, @@ -100,6 +103,12 @@ export function modifyUrl( } as UrlObject); } +/** + * Determine if a url is relative. Any url including a protocol, hostname, or + * port is not considered relative. This means that absolute *paths* are considered + * to be relative *urls* + * @public + */ export function isRelativeUrl(candidatePath: string) { // validate that `candidatePath` is not attempting a redirect to somewhere // outside of this Kibana install diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 6c2efeebc60c3..910313ac87059 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -40,6 +40,7 @@ import { CreatePackageJsonTask, CreateReadmeTask, CreateRpmPackageTask, + CreateStaticFsWithNodeModulesTask, DownloadNodeBuildsTask, ExtractNodeBuildsTask, InstallDependenciesTask, @@ -126,6 +127,7 @@ export async function buildDistributables(options) { await run(CleanTypescriptTask); await run(CleanExtraFilesFromModulesTask); await run(CleanEmptyFoldersTask); + await run(CreateStaticFsWithNodeModulesTask); /** * copy generic build outputs into platform-specific build diff --git a/src/dev/build/tasks/create_static_fs_with_node_modules_task.js b/src/dev/build/tasks/create_static_fs_with_node_modules_task.js new file mode 100644 index 0000000000000..0ab296fc5c163 --- /dev/null +++ b/src/dev/build/tasks/create_static_fs_with_node_modules_task.js @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import del from 'del'; +import globby from 'globby'; +import { resolve } from 'path'; +import { generateStaticFsVolume } from '@elastic/static-fs'; + +async function deletePathsList(list) { + for (const path of list) { + await del(path); + } +} + +async function getTopLevelNodeModulesFolders(rootDir) { + const nodeModulesFoldersForCwd = await globby(['**/node_modules', '!**/node_modules/**/*'], { + cwd: rootDir, + onlyDirectories: true, + }); + + return nodeModulesFoldersForCwd.map(folder => resolve(rootDir, folder)); +} + +export const CreateStaticFsWithNodeModulesTask = { + description: + 'Creating static filesystem with node_modules, patching entryPoints and deleting node_modules folder', + + async run(config, log, build) { + const rootDir = build.resolvePath('.'); + + // Get all the top node_modules folders + const nodeModulesFolders = await getTopLevelNodeModulesFolders(rootDir); + + // Define root entry points + const rootEntryPoints = [build.resolvePath('src/setup_node_env/index.js')]; + + // Creates the static filesystem with + // every node_module we have + const staticFsAddedPaths = await generateStaticFsVolume( + rootDir, + nodeModulesFolders, + rootEntryPoints + ); + + // Delete node_modules folder + await deletePathsList(staticFsAddedPaths); + }, +}; diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js index 8105fa8a7d5d4..0232ac4b1b5f3 100644 --- a/src/dev/build/tasks/index.js +++ b/src/dev/build/tasks/index.js @@ -25,6 +25,7 @@ export * from './create_archives_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_package_json_task'; export * from './create_readme_task'; +export * from './create_static_fs_with_node_modules_task'; export * from './install_dependencies_task'; export * from './license_file_task'; export * from './nodejs'; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8630221b3e94f..601dcc86352a7 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -35,8 +35,8 @@ export const IGNORE_FILE_GLOBS = [ '**/Gruntfile.js', 'tasks/config/**/*', '**/{Dockerfile,docker-compose.yml}', - 'x-pack/legacy/plugins/canvas/tasks/**/*', - 'x-pack/legacy/plugins/canvas/canvas_plugin_src/**/*', + 'x-pack/plugins/canvas/tasks/**/*', + 'x-pack/plugins/canvas/canvas_plugin_src/**/*', 'x-pack/plugins/monitoring/public/lib/jquery_flot/**/*', '**/.*', '**/{webpackShims,__mocks__}/**/*', @@ -48,7 +48,7 @@ export const IGNORE_FILE_GLOBS = [ 'vars/*', // Files in this directory must match a pre-determined name in some cases. - 'x-pack/legacy/plugins/canvas/.storybook/*', + 'x-pack/plugins/canvas/.storybook/*', // filename must match language code which requires capital letters '**/translations/*.json', diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js deleted file mode 100644 index 8820327d3adc0..0000000000000 --- a/src/dev/register_git_hook/register_git_hook.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import chalk from 'chalk'; -import { chmod, unlink, writeFile } from 'fs'; -import dedent from 'dedent'; -import normalizePath from 'normalize-path'; -import os from 'os'; -import { resolve } from 'path'; -import { promisify } from 'util'; -import SimpleGit from 'simple-git'; -import { REPO_ROOT } from '../constants'; - -const simpleGit = new SimpleGit(REPO_ROOT); - -const chmodAsync = promisify(chmod); -const gitRevParseAsync = promisify(simpleGit.revparse.bind(simpleGit)); -const unlinkAsync = promisify(unlink); -const writeFileAsync = promisify(writeFile); - -async function getPrecommitGitHookScriptPath(rootPath) { - // Retrieves the correct location for the .git dir for - // every git setup (including git worktree) - const gitDirPath = (await gitRevParseAsync(['--git-common-dir'])).trim(); - - return resolve(rootPath, gitDirPath, 'hooks/pre-commit'); -} - -function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) { - return dedent(` - #!/usr/bin/env bash - # - # ** THIS IS AN AUTO-GENERATED FILE ** - # ** PLEASE DO NOT CHANGE IT MANUALLY ** - # - # GENERATED BY ${__dirname} - # IF YOU WANNA CHANGE SOMETHING INTO THIS SCRIPT - # PLEASE RE-RUN 'yarn kbn bootstrap' or 'node scripts/register_git_hook' IN THE ROOT - # OF THE CURRENT PROJECT ${rootPath} - - # pre-commit script takes zero arguments: https://git-scm.com/docs/githooks#_pre_commit - - set -euo pipefail - - # Make it possible to terminate pre commit hook - # using ctrl-c so nothing else would happen or be - # sent to the output. - # - # The correct exit code on that situation - # according the linux documentation project is 130 - # https://www.tldp.org/LDP/abs/html/exitcodes.html - trap "exit 130" INT - - has_node() { - command -v node >/dev/null 2>&1 - } - - has_nvm() { - command -v nvm >/dev/null 2>&1 - } - - try_load_node_from_nvm_paths () { - # If nvm is not loaded, load it - has_node || { - NVM_SH="${nodeHome}/.nvm/nvm.sh" - - if [ "${platform}" == "darwin" ] && [ -s "$(brew --prefix nvm)/nvm.sh" ]; then - NVM_SH="$(brew --prefix nvm)/nvm.sh" - fi - - export NVM_DIR=${nodeHome}/.nvm - - [ -s "$NVM_SH" ] && \. "$NVM_SH" - - # If nvm has been loaded correctly, use project .nvmrc - has_nvm && nvm use - } - } - - extend_user_path() { - if [ "${platform}" == "win32" ]; then - export PATH="$PATH:/c/Program Files/nodejs" - else - export PATH="$PATH:/usr/local/bin:/usr/local" - try_load_node_from_nvm_paths - fi - } - - # Extend path with common path locations for node - # in order to make the hook working on git GUI apps - extend_user_path - - # Check if we have node js bin in path - has_node || { - echo "Can't found node bin in the PATH. Please update the PATH to proceed." - echo "If your PATH already has the node bin, maybe you are using some git GUI app." - echo "Can't found node bin in the PATH. Please update the PATH to proceed." - echo "If your PATH already has the node bin, maybe you are using some git GUI app not launched from the shell." - echo "In order to proceed, you need to config the PATH used by the application that are launching your git GUI app." - echo "If you are running macOS, you can do that using:" - echo "'sudo launchctl config user path /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'" - - exit 1 - } - - execute_precommit_hook() { - node scripts/precommit_hook || return 1 - - PRECOMMIT_FILE="./.git/hooks/pre-commit.local" - if [ -x "\${PRECOMMIT_FILE}" ]; then - echo "Executing local precommit hook found in \${PRECOMMIT_FILE}" - "$PRECOMMIT_FILE" || return 1 - fi - } - - execute_precommit_hook || { - echo "Pre-commit hook failed (add --no-verify to bypass)"; - echo ' For eslint failures you can try running \`node scripts/precommit_hook --fix\`'; - exit 1; - } - - exit 0 - `); -} - -export async function registerPrecommitGitHook(log) { - log.write(chalk.bold(`Registering Kibana pre-commit git hook...\n`)); - - try { - await writeGitHook( - await getPrecommitGitHookScriptPath(REPO_ROOT), - getKbnPrecommitGitHookScript(REPO_ROOT, normalizePath(os.homedir()), process.platform) - ); - } catch (e) { - log.write( - `${chalk.red('fail')} Kibana pre-commit git hook was not installed as an error occur.\n` - ); - throw e; - } - - log.write(`${chalk.green('success')} Kibana pre-commit git hook was installed successfully.\n`); -} - -async function writeGitHook(gitHookScriptPath, kbnHookScriptSource) { - try { - await unlinkAsync(gitHookScriptPath); - } catch (e) { - /* no-op */ - } - - await writeFileAsync(gitHookScriptPath, kbnHookScriptSource); - await chmodAsync(gitHookScriptPath, 0o755); -} diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 0e91f0a214a45..416702c56d852 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -20,7 +20,7 @@ export const storybookAliases = { advanced_ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', apm: 'x-pack/plugins/apm/scripts/storybook.js', - canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', + canvas: 'x-pack/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/scripts/storybook.js', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 51577456135d1..6664cf0d7366d 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -24,11 +24,11 @@ import { promisify } from 'util'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import mappings from './mappings.json'; -import { getUiSettingDefaults } from './ui_setting_defaults'; +import { getUiSettingDefaults } from './server/ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { kbnBaseUrl } from '../../../plugins/kibana_legacy/server'; const mkdirAsync = promisify(Fs.mkdir); @@ -67,33 +67,33 @@ export default function(kibana) { title: i18n.translate('kbn.discoverTitle', { defaultMessage: 'Discover', }), - order: -1003, + order: 2000, url: `${kbnBaseUrl}#/discover`, euiIconType: 'discoverApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:visualize', title: i18n.translate('kbn.visualizeTitle', { defaultMessage: 'Visualize', }), - order: -1002, + order: 7000, url: `${kbnBaseUrl}#/visualize`, euiIconType: 'visualizeApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:dashboard', title: i18n.translate('kbn.dashboardTitle', { defaultMessage: 'Dashboard', }), - order: -1001, + order: 1000, url: `${kbnBaseUrl}#/dashboards`, euiIconType: 'dashboardApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:dev_tools', @@ -108,7 +108,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js index b212ecf578dd1..de85bec011eeb 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js @@ -415,7 +415,7 @@ describe('Table Vis - AggTable Directive', function() { ); $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 1)}%`; + const percentage = `${round((counts[i] / total) * 100, 3)}%`; expect(value).to.be(percentage); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 6a36391c56b5c..2cba9fab7be22 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js similarity index 98% rename from src/legacy/core_plugins/kibana/ui_setting_defaults.js rename to src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 85b1956f45333..91c61886d216c 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ + import moment from 'moment-timezone'; import numeralLanguages from '@elastic/numeral/languages'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../plugins/data/common'; -import { isRelativeUrl } from '../../../core/utils'; +import { isRelativeUrl } from '../../../../core/server'; +import { DEFAULT_QUERY_LANGUAGE } from '../../../../plugins/data/common'; export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); @@ -1171,5 +1172,25 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, + pageNavigation: { + name: i18n.translate('kbn.advancedSettings.pageNavigationName', { + defaultMessage: 'Side nav style', + }), + value: 'modern', + description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { + defaultMessage: 'Change the style of navigation', + }), + type: 'select', + options: ['modern', 'legacy'], + optionLabels: { + modern: i18n.translate('kbn.advancedSettings.pageNavigationModern', { + defaultMessage: 'Modern', + }), + legacy: i18n.translate('kbn.advancedSettings.pageNavigationLegacy', { + defaultMessage: 'Legacy', + }), + }, + schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), + }, }; } diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts index 41a15dc4e0186..31926f658ec13 100644 --- a/src/legacy/core_plugins/timelion/index.ts +++ b/src/legacy/core_plugins/timelion/index.ts @@ -21,7 +21,7 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; import { LegacyPluginApi, LegacyPluginInitializer } from 'src/legacy/plugin_discovery/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../core/utils'; +import { DEFAULT_APP_CATEGORIES } from '../../../core/server'; const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { defaultMessage: 'experimental', @@ -54,11 +54,11 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl uiExports: { app: { title: 'Timelion', - order: -1000, + order: 8000, icon: 'plugins/timelion/icon.svg', euiIconType: 'timelionApp', main: 'plugins/timelion/app', - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: [resolve(__dirname, 'public/legacy')], diff --git a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap index 365f3afdab395..ad13256c8245a 100644 --- a/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap +++ b/src/legacy/ui/public/exit_full_screen/__snapshots__/exit_full_screen_button.test.js.snap @@ -29,13 +29,8 @@ exports[`is rendered 1`] = ` data-test-subj="exitFullScreenModeText" >
-

- Elastic Kibana -

Exit full screen diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap index cba8e85a65249..f0766df176c0d 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap @@ -4,12 +4,7 @@ exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; exports[`LabelTemplateFlyout should render normally 1`] = ` diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap index 849e307f7b527..fd697a2a4c70a 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap @@ -4,12 +4,7 @@ exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; exports[`UrlTemplateFlyout should render normally 1`] = ` diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap index 282e8e311d984..6991281dc86a9 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap @@ -2,13 +2,8 @@ exports[`ScriptingHelpFlyout should render normally 1`] = ` { renderBottomBar = () => { const areChangesInvalid = this.areChangesInvalid(); - const bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { - 'mgtAdvancedSettingsForm__bottomBar--pushForNav': - localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', - }); + + // TODO #64541 + // Delete these classes + let bottomBarClasses = ''; + const pageNav = this.props.settings.general.find(setting => setting.name === 'pageNavigation'); + + if (pageNav?.value === 'legacy') { + bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { + 'mgtAdvancedSettingsForm__bottomBar--pushForNav': + localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', + }); + } return ( { unsubscribeResizer(); - mappings.clearSubscriptions(); + clearSubscriptions(); window.removeEventListener('hashchange', onHashChange); }; }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]); diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/settings.tsx index e34cfcac8096b..81938a83435de 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/settings.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; // @ts-ignore -import mappings from '../../lib/mappings/mappings'; +import { retrieveAutoCompleteInfo } from '../../lib/mappings/mappings'; import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings, Settings as SettingsService } from '../../services'; @@ -33,7 +33,7 @@ const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToo }; const refreshAutocompleteSettings = (settings: SettingsService, selectedSettings: any) => { - mappings.retrieveAutoCompleteInfo(settings, selectedSettings); + retrieveAutoCompleteInfo(settings, selectedSettings); }; const fetchAutocompleteSettingsIfNeeded = ( @@ -61,10 +61,10 @@ const fetchAutocompleteSettingsIfNeeded = ( }, {} ); - mappings.retrieveAutoCompleteInfo(settings, changedSettings); + retrieveAutoCompleteInfo(settings, changedSettings); } else if (isPollingChanged && newSettings.polling) { // If the user has turned polling on, then we'll fetch all selected autocomplete settings. - mappings.retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); } } }; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index dde793d9b9691..f0ce61f1d3401 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -24,7 +24,7 @@ import { sendRequestToES } from './send_request_to_es'; import { track } from './track'; // @ts-ignore -import mappings from '../../../lib/mappings/mappings'; +import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToES = () => { const { @@ -73,7 +73,7 @@ export const useSendCurrentRequestToES = () => { // or templates may have changed, so we'll need to update this data. Assume that if // the user disables polling they're trying to optimize performance or otherwise // preserve resources, so they won't want this request sent either. - mappings.retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); } dispatch({ diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js index 5c86b0a1d2092..1db9ca7bc0a86 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js @@ -17,7 +17,7 @@ * under the License. */ import '../legacy_core_editor.test.mocks'; -const $ = require('jquery'); +import $ from 'jquery'; import RowParser from '../../../../lib/row_parser'; import ace from 'brace'; import { createReadOnlyAceEditor } from '../create_readonly'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js index d763db7ae5d79..77b4ba8cea6ff 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js @@ -19,20 +19,21 @@ import ace from 'brace'; import { workerModule } from './worker'; +import { ScriptMode } from './script'; const oop = ace.acequire('ace/lib/oop'); const TextMode = ace.acequire('ace/mode/text').Mode; -const ScriptMode = require('./script').ScriptMode; + const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient; const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; -const HighlightRules = require('./input_highlight_rules').InputHighlightRules; +import { InputHighlightRules } from './input_highlight_rules'; export function Mode() { - this.$tokenizer = new AceTokenizer(new HighlightRules().getRules()); + this.$tokenizer = new AceTokenizer(new InputHighlightRules().getRules()); this.$outdent = new MatchingBraceOutdent(); this.$behaviour = new CstyleBehaviour(); this.foldingRules = new CStyleFoldMode(); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js index 29f192f4ea858..1558cf0cb5554 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js @@ -17,7 +17,7 @@ * under the License. */ -const ace = require('brace'); +import ace from 'brace'; import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; export function addEOL(tokens, reg, nextIfEOL, normalNext) { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js index 40e3128e396a3..5ad34532d1861 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js @@ -18,11 +18,11 @@ */ import ace from 'brace'; -require('./output_highlight_rules'); + +import { OutputJsonHighlightRules } from './output_highlight_rules'; const oop = ace.acequire('ace/lib/oop'); const JSONMode = ace.acequire('ace/mode/json').Mode; -const HighlightRules = require('./output_highlight_rules').OutputJsonHighlightRules; const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; @@ -30,7 +30,7 @@ ace.acequire('ace/worker/worker_client'); const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; export function Mode() { - this.$tokenizer = new AceTokenizer(new HighlightRules().getRules()); + this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); this.$outdent = new MatchingBraceOutdent(); this.$behaviour = new CstyleBehaviour(); this.foldingRules = new CStyleFoldMode(); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js index c9d538ab6ceb1..448fd847aeacd 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js @@ -17,7 +17,7 @@ * under the License. */ -const ace = require('brace'); +import ace from 'brace'; import 'brace/mode/json'; import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js index f79a171c65082..8a0eb9a03480b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js @@ -18,7 +18,7 @@ */ /* eslint import/no-unresolved: 0 */ -const ace = require('brace'); +import ace from 'brace'; ace.define('ace/theme/sense-dark', ['require', 'exports', 'module'], function(require, exports) { exports.isDark = true; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index c5a0c2ebddf71..edd09885c1ad2 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -19,10 +19,10 @@ import '../sense_editor.test.mocks'; import { create } from '../create'; import _ from 'lodash'; -const $ = require('jquery'); +import $ from 'jquery'; -const kb = require('../../../../lib/kb/kb'); -const mappings = require('../../../../lib/mappings/mappings'); +import * as kb from '../../../../lib/kb/kb'; +import * as mappings from '../../../../lib/mappings/mappings'; describe('Integration', () => { let senseEditor; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js index 34b4cad7fbb6b..219e6262ab346 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js @@ -23,7 +23,7 @@ import _ from 'lodash'; import { create } from '../create'; import { collapseLiteralStrings } from '../../../../../../es_ui_shared/public'; -const editorInput1 = require('./editor_input1.txt'); +import editorInput1 from './editor_input1.txt'; describe('Editor', () => { let input; diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js index 0758a75695566..ebde8c39cffbc 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { URL_PATH_END_MARKER, UrlPatternMatcher, diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js index 72fce53c4f1fe..286aefcd133a0 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { UrlParams } from '../../autocomplete/url_params'; import { populateContext } from '../../autocomplete/engine'; diff --git a/src/plugins/console/public/lib/autocomplete/body_completer.js b/src/plugins/console/public/lib/autocomplete/body_completer.js index 1aa315c50b9bf..9bb1f14a6266a 100644 --- a/src/plugins/console/public/lib/autocomplete/body_completer.js +++ b/src/plugins/console/public/lib/autocomplete/body_completer.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { WalkingState, walkTokenPath, wrapComponentWithDefaults } from './engine'; import { ConstantComponent, diff --git a/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js index 0574ffbcfc3a9..80f65406cf5d3 100644 --- a/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js @@ -17,11 +17,11 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getFields } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function FieldGenerator(context) { - return _.map(mappings.getFields(context.indices, context.types), function(field) { + return _.map(getFields(context.indices, context.types), function(field) { return { name: field.name, meta: field.type }; }); } diff --git a/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js index 03d67c9e27ee8..ec6f24253e78d 100644 --- a/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js @@ -17,14 +17,14 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getIndices } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function nonValidIndexType(token) { return !(token === '_all' || token[0] !== '_'); } export class IndexAutocompleteComponent extends ListComponent { constructor(name, parent, multiValued) { - super(name, mappings.getIndices, parent, multiValued); + super(name, getIndices, parent, multiValued); } validateTokens(tokens) { if (!this.multiValued && tokens.length > 1) { diff --git a/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js index cc62a2f9eeea6..14141980d493d 100644 --- a/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import mappings from '../../mappings/mappings'; +import { getTemplates } from '../../mappings/mappings'; import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent, true, true); + super(name, getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js index 507817c1ed8c5..03d85eccaf385 100644 --- a/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js @@ -18,9 +18,9 @@ */ import _ from 'lodash'; import { ListComponent } from './list_component'; -import mappings from '../../mappings/mappings'; +import { getTypes } from '../../mappings/mappings'; function TypeGenerator(context) { - return mappings.getTypes(context.indices); + return getTypes(context.indices); } function nonValidIndexType(token) { return !(token === '_all' || token[0] !== '_'); diff --git a/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js index 26b7bd5c48c99..14b77d4e70625 100644 --- a/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js @@ -17,14 +17,14 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getIndices } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function nonValidUsernameType(token) { return token[0] === '_'; } export class UsernameAutocompleteComponent extends ListComponent { constructor(name, parent, multiValued) { - super(name, mappings.getIndices, parent, multiValued); + super(name, getIndices, parent, multiValued); } validateTokens(tokens) { if (!this.multiValued && tokens.length > 1) { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7b64d91c95374..ae943a74ffb3a 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; export function wrapComponentWithDefaults(component, defaults) { const originalGetTerms = component.getTerms; diff --git a/src/plugins/console/public/lib/autocomplete/url_params.js b/src/plugins/console/public/lib/autocomplete/url_params.js index 3f05b9ce85aab..0519a2daade87 100644 --- a/src/plugins/console/public/lib/autocomplete/url_params.js +++ b/src/plugins/console/public/lib/autocomplete/url_params.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { ConstantComponent, ListComponent, SharedComponent } from './components'; export class ParamComponent extends ConstantComponent { diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js index 49a54eaefa9ef..6a8caebfc6874 100644 --- a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js +++ b/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js @@ -17,15 +17,15 @@ * under the License. */ -const _ = require('lodash'); -const curl = require('../curl'); +import _ from 'lodash'; +import { detectCURL, parseCURL } from '../curl'; import curlTests from './curl_parsing.txt'; describe('CURL', () => { const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }']; _.each(notCURLS, function(notCURL, i) { test('cURL Detection - broken strings ' + i, function() { - expect(curl.detectCURL(notCURL)).toEqual(false); + expect(detectCURL(notCURL)).toEqual(false); }); }); @@ -39,8 +39,8 @@ describe('CURL', () => { const response = fixture[2].trim(); test('cURL Detection - ' + name, function() { - expect(curl.detectCURL(curlText)).toBe(true); - const r = curl.parseCURL(curlText); + expect(detectCURL(curlText)).toBe(true); + const r = parseCURL(curlText); expect(r).toEqual(response); }); }); diff --git a/src/plugins/console/public/lib/kb/__tests__/kb.test.js b/src/plugins/console/public/lib/kb/__tests__/kb.test.js index c2c69314a172d..c80f5671449b3 100644 --- a/src/plugins/console/public/lib/kb/__tests__/kb.test.js +++ b/src/plugins/console/public/lib/kb/__tests__/kb.test.js @@ -21,8 +21,8 @@ import _ from 'lodash'; import { populateContext } from '../../autocomplete/engine'; import '../../../application/models/sense_editor/sense_editor.test.mocks'; -const kb = require('../../kb'); -const mappings = require('../../mappings/mappings'); +import * as kb from '../../kb'; +import * as mappings from '../../mappings/mappings'; describe('Knowledge base', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/kb/api.js b/src/plugins/console/public/lib/kb/api.js index eeec87060b770..c418a7cb414ef 100644 --- a/src/plugins/console/public/lib/kb/api.js +++ b/src/plugins/console/public/lib/kb/api.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { UrlPatternMatcher } from '../autocomplete/components'; import { UrlParams } from '../autocomplete/url_params'; import { diff --git a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js b/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js index 27b3ce26b5588..292b1b4fb1bf5 100644 --- a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js @@ -17,7 +17,7 @@ * under the License. */ import '../../../application/models/sense_editor/sense_editor.test.mocks'; -const mappings = require('../mappings'); +import * as mappings from '../mappings'; describe('Mappings', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index 330147118d42c..b5bcc2b105996 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -17,9 +17,9 @@ * under the License. */ -const $ = require('jquery'); -const _ = require('lodash'); -const es = require('../es/es'); +import $ from 'jquery'; +import _ from 'lodash'; +import * as es from '../es/es'; // NOTE: If this value ever changes to be a few seconds or less, it might introduce flakiness // due to timing issues in our app.js tests. @@ -32,7 +32,7 @@ let templates = []; const mappingObj = {}; -function expandAliases(indicesOrAliases) { +export function expandAliases(indicesOrAliases) { // takes a list of indices or aliases or a string which may be either and returns a list of indices // returns a list for multiple values or a string for a single. @@ -60,11 +60,11 @@ function expandAliases(indicesOrAliases) { return ret.length > 1 ? ret : ret[0]; } -function getTemplates() { +export function getTemplates() { return [...templates]; } -function getFields(indices, types) { +export function getFields(indices, types) { // get fields for indices and types. Both can be a list, a string or null (meaning all). let ret = []; indices = expandAliases(indices); @@ -103,7 +103,7 @@ function getFields(indices, types) { }); } -function getTypes(indices) { +export function getTypes(indices) { let ret = []; indices = expandAliases(indices); if (typeof indices === 'string') { @@ -129,7 +129,7 @@ function getTypes(indices) { return _.uniq(ret); } -function getIndices(includeAliases) { +export function getIndices(includeAliases) { const ret = []; $.each(perIndexTypes, function(index) { ret.push(index); @@ -200,7 +200,7 @@ function loadTemplates(templatesObject = {}) { templates = Object.keys(templatesObject); } -function loadMappings(mappings) { +export function loadMappings(mappings) { perIndexTypes = {}; $.each(mappings, function(index, indexMapping) { @@ -224,7 +224,7 @@ function loadMappings(mappings) { }); } -function loadAliases(aliases) { +export function loadAliases(aliases) { perAliasIndexes = {}; $.each(aliases || {}, function(index, omdexAliases) { // verify we have an index defined. useful when mapping loading is disabled @@ -246,7 +246,7 @@ function loadAliases(aliases) { perAliasIndexes._all = getIndices(false); } -function clear() { +export function clear() { perIndexTypes = {}; perAliasIndexes = {}; templates = []; @@ -285,7 +285,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { // unchanged alone (both selected and unselected). // 3. Poll: Use saved. Fetch selected. Ignore unselected. -function clearSubscriptions() { +export function clearSubscriptions() { if (pollTimeoutId) { clearTimeout(pollTimeoutId); } @@ -296,7 +296,7 @@ function clearSubscriptions() { * @param settings Settings A way to retrieve the current settings * @param settingsToRetrieve any */ -function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { +export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { clearSubscriptions(); const mappingPromise = retrieveSettings('fields', settingsToRetrieve); @@ -341,16 +341,3 @@ function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { }, POLL_INTERVAL); }); } - -export default { - getFields, - getTemplates, - getIndices, - getTypes, - loadMappings, - loadAliases, - expandAliases, - clear, - retrieveAutoCompleteInfo, - clearSubscriptions, -}; diff --git a/src/plugins/console/public/lib/utils/__tests__/utils.test.js b/src/plugins/console/public/lib/utils/__tests__/utils.test.js index 6115be3c84ed9..3a2e6a54c1328 100644 --- a/src/plugins/console/public/lib/utils/__tests__/utils.test.js +++ b/src/plugins/console/public/lib/utils/__tests__/utils.test.js @@ -17,7 +17,7 @@ * under the License. */ -const utils = require('../'); +import * as utils from '../'; describe('Utils class', () => { test('extract deprecation messages', function() { diff --git a/src/plugins/dashboard/public/application/_dashboard_app.scss b/src/plugins/dashboard/public/application/_dashboard_app.scss index 8f389bb031df1..719d0a3268b5d 100644 --- a/src/plugins/dashboard/public/application/_dashboard_app.scss +++ b/src/plugins/dashboard/public/application/_dashboard_app.scss @@ -1,7 +1,8 @@ .dshAppContainer { display: flex; flex-direction: column; - height: 100%; + height: 100%; // TODO #64541 - can delete this + flex: 1; } .dshStartScreen { diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 8346fd900caef..7e25d80c9d619 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -177,6 +177,7 @@ export class DashboardContainer extends Container []) as any, getEmbeddableFactories: start.getEmbeddableFactories, diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html index f473e91af7ae9..f57c10d1a48dd 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html @@ -8,4 +8,4 @@ listing-limit="listingLimit" hide-write-controls="hideWriteControls" initial-filter="initialFilter" -/> +> diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index 40231de7597f1..6eb85faeea014 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -38,6 +38,7 @@ import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { KibanaContextProvider } from '../../../../kibana_react/public'; import { uiActionsPluginMock } from '../../../../ui_actions/public/mocks'; +import { applicationServiceMock } from '../../../../../core/public/mocks'; test('DashboardContainer in edit mode shows edit mode actions', async () => { const inspector = inspectorPluginMock.createStartContract(); @@ -56,7 +57,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { const initialInput = getSampleDashboardInput({ viewMode: ViewMode.VIEW }); const options: DashboardContainerOptions = { - application: {} as any, + application: applicationServiceMock.createStartContract(), embeddable: start, notifications: {} as any, overlays: {} as any, @@ -84,7 +85,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => null) as any} notifications={{} as any} - application={{} as any} + application={options.application} overlays={{} as any} inspector={inspector} SavedObjectFinder={() => null} diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 0820ebd371004..490ddbed933d9 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -18,7 +18,6 @@ */ export const DashboardConstants = { - ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard', LANDING_PAGE_PATH: '/dashboards', CREATE_NEW_DASHBOARD_URL: '/dashboard', ADD_EMBEDDABLE_ID: 'addEmbeddableId', diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 7c7d7609cc82f..2af29d3600246 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -118,7 +118,51 @@ import { aggHistogram } from './buckets/histogram_fn'; import { aggDateHistogram } from './buckets/date_histogram_fn'; import { aggTerms } from './buckets/terms_fn'; +/** Metrics: **/ +import { aggAvg } from './metrics/avg_fn'; +import { aggBucketAvg } from './metrics/bucket_avg_fn'; +import { aggBucketMax } from './metrics/bucket_max_fn'; +import { aggBucketMin } from './metrics/bucket_min_fn'; +import { aggBucketSum } from './metrics/bucket_sum_fn'; +import { aggCardinality } from './metrics/cardinality_fn'; +import { aggCount } from './metrics/count_fn'; +import { aggCumulativeSum } from './metrics/cumulative_sum_fn'; +import { aggDerivative } from './metrics/derivative_fn'; +import { aggGeoBounds } from './metrics/geo_bounds_fn'; +import { aggGeoCentroid } from './metrics/geo_centroid_fn'; +import { aggMax } from './metrics/max_fn'; +import { aggMedian } from './metrics/median_fn'; +import { aggMin } from './metrics/min_fn'; +import { aggMovingAvg } from './metrics/moving_avg_fn'; +import { aggPercentileRanks } from './metrics/percentile_ranks_fn'; +import { aggPercentiles } from './metrics/percentiles_fn'; +import { aggSerialDiff } from './metrics/serial_diff_fn'; +import { aggStdDeviation } from './metrics/std_deviation_fn'; +import { aggSum } from './metrics/sum_fn'; +import { aggTopHit } from './metrics/top_hit_fn'; + export const getAggTypesFunctions = () => [ + aggAvg, + aggBucketAvg, + aggBucketMax, + aggBucketMin, + aggBucketSum, + aggCardinality, + aggCount, + aggCumulativeSum, + aggDerivative, + aggGeoBounds, + aggGeoCentroid, + aggMax, + aggMedian, + aggMin, + aggMovingAvg, + aggPercentileRanks, + aggPercentiles, + aggSerialDiff, + aggStdDeviation, + aggSum, + aggTopHit, aggFilter, aggFilters, aggSignificantTerms, diff --git a/src/plugins/data/public/search/aggs/metrics/avg.ts b/src/plugins/data/public/search/aggs/metrics/avg.ts index d53ce8d3fc489..96be3e849a3e8 100644 --- a/src/plugins/data/public/search/aggs/metrics/avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/avg.ts @@ -22,11 +22,16 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const averageTitle = i18n.translate('data.search.aggs.metrics.averageTitle', { defaultMessage: 'Average', }); +export interface AggParamsAvg extends BaseAggParams { + field: string; +} + export interface AvgMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/avg_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/avg_fn.test.ts new file mode 100644 index 0000000000000..0e2ee00df49dd --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/avg_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggAvg } from './avg_fn'; + +describe('agg_expression_functions', () => { + describe('aggAvg', () => { + const fn = functionWrapper(aggAvg()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "avg", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/avg_fn.ts new file mode 100644 index 0000000000000..c370623b2752a --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/avg_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggAvg'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggAvg = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.avg.help', { + defaultMessage: 'Generates a serialized agg config for a Avg agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.avg.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.avg.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.avg.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.avg.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.avg.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.avg.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.AVG, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts index 2c32ebc671539..ded17eebf465b 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts @@ -23,8 +23,14 @@ import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsBucketAvg extends BaseAggParams { + customMetric?: AggConfigSerialized; + customBucket?: AggConfigSerialized; +} + export interface BucketAvgMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.test.ts new file mode 100644 index 0000000000000..7e08bc9954510 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.test.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggBucketAvg } from './bucket_avg_fn'; + +describe('agg_expression_functions', () => { + describe('aggBucketAvg', () => { + const fn = functionWrapper(aggBucketAvg()); + + test('handles customMetric and customBucket as a subexpression', () => { + const actual = fn({ + customMetric: fn({}), + customBucket: fn({}), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "customBucket": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "avg_bucket", + }, + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "avg_bucket", + }, + "json": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts new file mode 100644 index 0000000000000..56643a2df54bd --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggBucketAvg'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign< + AggArgs, + { customBucket?: AggExpressionType; customMetric?: AggExpressionType } +>; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggBucketAvg = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.bucket_avg.help', { + defaultMessage: 'Generates a serialized agg config for a Avg Bucket agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.bucket_avg.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customBucket: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.customBucket.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.customMetric.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_avg.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.AVG_BUCKET, + params: { + ...rest, + customBucket: args.customBucket?.value, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts index 1e57a2dd8e38e..dde328008b88a 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts @@ -22,8 +22,14 @@ import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsBucketMax extends BaseAggParams { + customMetric?: AggConfigSerialized; + customBucket?: AggConfigSerialized; +} + export interface BucketMaxMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.test.ts new file mode 100644 index 0000000000000..b789bdf51ebd5 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.test.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggBucketMax } from './bucket_max_fn'; + +describe('agg_expression_functions', () => { + describe('aggBucketMax', () => { + const fn = functionWrapper(aggBucketMax()); + + test('handles customMetric and customBucket as a subexpression', () => { + const actual = fn({ + customMetric: fn({}), + customBucket: fn({}), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "customBucket": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "max_bucket", + }, + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "max_bucket", + }, + "json": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts new file mode 100644 index 0000000000000..896e9cf839605 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggBucketMax'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign< + AggArgs, + { customBucket?: AggExpressionType; customMetric?: AggExpressionType } +>; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggBucketMax = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.bucket_max.help', { + defaultMessage: 'Generates a serialized agg config for a Max Bucket agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.bucket_max.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customBucket: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.customBucket.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.customMetric.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_max.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MAX_BUCKET, + params: { + ...rest, + customBucket: args.customBucket?.value, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts index 0484af23a7141..9949524ce6110 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts @@ -22,8 +22,14 @@ import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsBucketMin extends BaseAggParams { + customMetric?: AggConfigSerialized; + customBucket?: AggConfigSerialized; +} + export interface BucketMinMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.test.ts new file mode 100644 index 0000000000000..6ebc83417813b --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.test.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggBucketMin } from './bucket_min_fn'; + +describe('agg_expression_functions', () => { + describe('aggBucketMin', () => { + const fn = functionWrapper(aggBucketMin()); + + test('handles customMetric and customBucket as a subexpression', () => { + const actual = fn({ + customMetric: fn({}), + customBucket: fn({}), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "customBucket": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "min_bucket", + }, + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "min_bucket", + }, + "json": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts new file mode 100644 index 0000000000000..2ae3d9211227a --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggBucketMin'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign< + AggArgs, + { customBucket?: AggExpressionType; customMetric?: AggExpressionType } +>; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggBucketMin = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.bucket_min.help', { + defaultMessage: 'Generates a serialized agg config for a Min Bucket agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.bucket_min.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customBucket: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.customBucket.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.customMetric.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_min.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MIN_BUCKET, + params: { + ...rest, + customBucket: args.customBucket?.value, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts index 0a4d29a18a980..e69ae5798c6e1 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts @@ -22,8 +22,14 @@ import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsBucketSum extends BaseAggParams { + customMetric?: AggConfigSerialized; + customBucket?: AggConfigSerialized; +} + export interface BucketSumMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.test.ts new file mode 100644 index 0000000000000..71549f41b1d15 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.test.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggBucketSum } from './bucket_sum_fn'; + +describe('agg_expression_functions', () => { + describe('aggBucketSum', () => { + const fn = functionWrapper(aggBucketSum()); + + test('handles customMetric and customBucket as a subexpression', () => { + const actual = fn({ + customMetric: fn({}), + customBucket: fn({}), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "customBucket": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "sum_bucket", + }, + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "sum_bucket", + }, + "json": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts new file mode 100644 index 0000000000000..eceb11a90f293 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggBucketSum'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign< + AggArgs, + { customBucket?: AggExpressionType; customMetric?: AggExpressionType } +>; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggBucketSum = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.bucket_sum.help', { + defaultMessage: 'Generates a serialized agg config for a Sum Bucket agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.bucket_sum.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customBucket: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.customBucket.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.customMetric.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.bucket_sum.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.SUM_BUCKET, + params: { + ...rest, + customBucket: args.customBucket?.value, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality.ts b/src/plugins/data/public/search/aggs/metrics/cardinality.ts index 10b6b5aff1abd..af594195fe027 100644 --- a/src/plugins/data/public/search/aggs/metrics/cardinality.ts +++ b/src/plugins/data/public/search/aggs/metrics/cardinality.ts @@ -22,11 +22,16 @@ import { MetricAggType, IMetricAggConfig } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTitle', { defaultMessage: 'Unique Count', }); +export interface AggParamsCardinality extends BaseAggParams { + field: string; +} + export interface CardinalityMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.test.ts new file mode 100644 index 0000000000000..4008819018ee5 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggCardinality } from './cardinality_fn'; + +describe('agg_expression_functions', () => { + describe('aggCardinality', () => { + const fn = functionWrapper(aggCardinality()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "cardinality", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts new file mode 100644 index 0000000000000..f30429993638f --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggCardinality'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggCardinality = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.cardinality.help', { + defaultMessage: 'Generates a serialized agg config for a Cardinality agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cardinality.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.cardinality.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cardinality.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.cardinality.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cardinality.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cardinality.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.CARDINALITY, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/count_fn.test.ts new file mode 100644 index 0000000000000..846feb9296fca --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/count_fn.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggCount } from './count_fn'; + +describe('agg_expression_functions', () => { + describe('aggCount', () => { + const fn = functionWrapper(aggCount()); + + test('correctly creates agg type', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "json": undefined, + }, + "schema": undefined, + "type": "count", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/count_fn.ts b/src/plugins/data/public/search/aggs/metrics/count_fn.ts new file mode 100644 index 0000000000000..f4c7e8e854230 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/count_fn.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggCount'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggCount = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.count.help', { + defaultMessage: 'Generates a serialized agg config for a Count agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.count.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.count.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.count.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.count.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.COUNT, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts index 8ca922e144a1f..67e907239799a 100644 --- a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts @@ -22,8 +22,15 @@ import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsCumulativeSum extends BaseAggParams { + buckets_path: string; + customMetric?: AggConfigSerialized; + metricAgg?: string; +} + export interface CumulativeSumMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.test.ts new file mode 100644 index 0000000000000..3cf53e3da153e --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggCumulativeSum } from './cumulative_sum_fn'; + +describe('agg_expression_functions', () => { + describe('aggCumulativeSum', () => { + const fn = functionWrapper(aggCumulativeSum()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "cumulative_sum", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + metricAgg: 'sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": "sum", + }, + "schema": undefined, + "type": "cumulative_sum", + }, + } + `); + }); + + test('handles customMetric as a subexpression', () => { + const actual = fn({ + customMetric: fn({ buckets_path: 'the_sum' }), + buckets_path: 'the_sum', + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "cumulative_sum", + }, + "json": undefined, + "metricAgg": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + buckets_path: 'the_sum', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + buckets_path: 'the_sum', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts new file mode 100644 index 0000000000000..950df03b10134 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggCumulativeSum'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggCumulativeSum = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.cumulative_sum.help', { + defaultMessage: 'Generates a serialized agg config for a Cumulative Sum agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + metricAgg: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.metricAgg.help', { + defaultMessage: + 'Id for finding agg config to use for building parent pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.customMetric.help', { + defaultMessage: 'Agg config to use for building parent pipeline aggregations', + }), + }, + buckets_path: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.buckets_path.help', { + defaultMessage: 'Path to the metric of interest', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.cumulative_sum.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.CUMULATIVE_SUM, + params: { + ...rest, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/derivative.ts b/src/plugins/data/public/search/aggs/metrics/derivative.ts index 5752a72c846aa..edb907ca4ed41 100644 --- a/src/plugins/data/public/search/aggs/metrics/derivative.ts +++ b/src/plugins/data/public/search/aggs/metrics/derivative.ts @@ -22,8 +22,15 @@ import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsDerivative extends BaseAggParams { + buckets_path: string; + customMetric?: AggConfigSerialized; + metricAgg?: string; +} + export interface DerivativeMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/derivative_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/derivative_fn.test.ts new file mode 100644 index 0000000000000..79ea7292104ee --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/derivative_fn.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggDerivative } from './derivative_fn'; + +describe('agg_expression_functions', () => { + describe('aggDerivative', () => { + const fn = functionWrapper(aggDerivative()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "derivative", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + metricAgg: 'sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": "sum", + }, + "schema": undefined, + "type": "derivative", + }, + } + `); + }); + + test('handles customMetric as a subexpression', () => { + const actual = fn({ + customMetric: fn({ buckets_path: 'the_sum' }), + buckets_path: 'the_sum', + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "derivative", + }, + "json": undefined, + "metricAgg": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + buckets_path: 'the_sum', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + buckets_path: 'the_sum', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts b/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts new file mode 100644 index 0000000000000..90b88b4de2712 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggDerivative'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggDerivative = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.derivative.help', { + defaultMessage: 'Generates a serialized agg config for a Derivative agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.derivative.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.derivative.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.derivative.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + metricAgg: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.derivative.metricAgg.help', { + defaultMessage: + 'Id for finding agg config to use for building parent pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.derivative.customMetric.help', { + defaultMessage: 'Agg config to use for building parent pipeline aggregations', + }), + }, + buckets_path: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.derivative.buckets_path.help', { + defaultMessage: 'Path to the metric of interest', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.derivative.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.derivative.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.DERIVATIVE, + params: { + ...rest, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts b/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts index 00927ebba56bf..864e97ca8dfe7 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts @@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsGeoBounds extends BaseAggParams { + field: string; +} export interface GeoBoundsMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.test.ts new file mode 100644 index 0000000000000..96bd31916784a --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggGeoBounds } from './geo_bounds_fn'; + +describe('agg_expression_functions', () => { + describe('aggGeoBounds', () => { + const fn = functionWrapper(aggGeoBounds()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "geo_bounds", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts new file mode 100644 index 0000000000000..8ba71a098fc70 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggGeoBounds'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggGeoBounds = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.geo_bounds.help', { + defaultMessage: 'Generates a serialized agg config for a Geo Bounds agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_bounds.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.geo_bounds.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_bounds.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.geo_bounds.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_bounds.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_bounds.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.GEO_BOUNDS, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts b/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts index a4b084f794a5d..2bbb6b2de8d87 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts @@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsGeoCentroid extends BaseAggParams { + field: string; +} export interface GeoCentroidMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.test.ts new file mode 100644 index 0000000000000..bf9a4548bafbf --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggGeoCentroid } from './geo_centroid_fn'; + +describe('agg_expression_functions', () => { + describe('aggGeoCentroid', () => { + const fn = functionWrapper(aggGeoCentroid()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "geo_centroid", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts new file mode 100644 index 0000000000000..464f9b535cd8b --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggGeoCentroid'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggGeoCentroid = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.geo_centroid.help', { + defaultMessage: 'Generates a serialized agg config for a Geo Centroid agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_centroid.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.geo_centroid.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_centroid.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.geo_centroid.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_centroid.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.geo_centroid.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.GEO_CENTROID, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/index.ts b/src/plugins/data/public/search/aggs/metrics/index.ts index eb93e99427f65..ef7de68b05de9 100644 --- a/src/plugins/data/public/search/aggs/metrics/index.ts +++ b/src/plugins/data/public/search/aggs/metrics/index.ts @@ -21,3 +21,23 @@ export * from './metric_agg_type'; export * from './metric_agg_types'; export * from './lib/parent_pipeline_agg_helper'; export * from './lib/sibling_pipeline_agg_helper'; +export { AggParamsAvg } from './avg'; +export { AggParamsCardinality } from './cardinality'; +export { AggParamsGeoBounds } from './geo_bounds'; +export { AggParamsGeoCentroid } from './geo_centroid'; +export { AggParamsMax } from './max'; +export { AggParamsMedian } from './median'; +export { AggParamsMin } from './min'; +export { AggParamsStdDeviation } from './std_deviation'; +export { AggParamsSum } from './sum'; +export { AggParamsBucketAvg } from './bucket_avg'; +export { AggParamsBucketMax } from './bucket_max'; +export { AggParamsBucketMin } from './bucket_min'; +export { AggParamsBucketSum } from './bucket_sum'; +export { AggParamsCumulativeSum } from './cumulative_sum'; +export { AggParamsDerivative } from './derivative'; +export { AggParamsMovingAvg } from './moving_avg'; +export { AggParamsPercentileRanks } from './percentile_ranks'; +export { AggParamsPercentiles } from './percentiles'; +export { AggParamsSerialDiff } from './serial_diff'; +export { AggParamsTopHit } from './top_hit'; diff --git a/src/plugins/data/public/search/aggs/metrics/max.ts b/src/plugins/data/public/search/aggs/metrics/max.ts index 88e8b485cb73f..49cbfba5a269d 100644 --- a/src/plugins/data/public/search/aggs/metrics/max.ts +++ b/src/plugins/data/public/search/aggs/metrics/max.ts @@ -22,11 +22,16 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const maxTitle = i18n.translate('data.search.aggs.metrics.maxTitle', { defaultMessage: 'Max', }); +export interface AggParamsMax extends BaseAggParams { + field: string; +} + export interface MaxMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/max_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/max_fn.test.ts new file mode 100644 index 0000000000000..156b51ca54af5 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/max_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggMax } from './max_fn'; + +describe('agg_expression_functions', () => { + describe('aggMax', () => { + const fn = functionWrapper(aggMax()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "max", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/max_fn.ts b/src/plugins/data/public/search/aggs/metrics/max_fn.ts new file mode 100644 index 0000000000000..1d68c8919fca8 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/max_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggMax'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggMax = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.max.help', { + defaultMessage: 'Generates a serialized agg config for a Max agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.max.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.max.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.max.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.max.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.max.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.max.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MAX, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/median.ts b/src/plugins/data/public/search/aggs/metrics/median.ts index a398f017602b0..725fdcb2400d1 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.ts @@ -22,11 +22,16 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const medianTitle = i18n.translate('data.search.aggs.metrics.medianTitle', { defaultMessage: 'Median', }); +export interface AggParamsMedian extends BaseAggParams { + field: string; +} + export interface MedianMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/median_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/median_fn.test.ts new file mode 100644 index 0000000000000..69200c35426c8 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/median_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggMedian } from './median_fn'; + +describe('agg_expression_functions', () => { + describe('aggMedian', () => { + const fn = functionWrapper(aggMedian()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "median", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/median_fn.ts b/src/plugins/data/public/search/aggs/metrics/median_fn.ts new file mode 100644 index 0000000000000..2e8e89992136e --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/median_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggMedian'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggMedian = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.median.help', { + defaultMessage: 'Generates a serialized agg config for a Median agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.median.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.median.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.median.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.median.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.median.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.median.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MEDIAN, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/min.ts b/src/plugins/data/public/search/aggs/metrics/min.ts index aae16f357186c..0f52aa8a4f788 100644 --- a/src/plugins/data/public/search/aggs/metrics/min.ts +++ b/src/plugins/data/public/search/aggs/metrics/min.ts @@ -22,11 +22,16 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const minTitle = i18n.translate('data.search.aggs.metrics.minTitle', { defaultMessage: 'Min', }); +export interface AggParamsMin extends BaseAggParams { + field: string; +} + export interface MinMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/min_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/min_fn.test.ts new file mode 100644 index 0000000000000..ef32d086e41f7 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/min_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggMin } from './min_fn'; + +describe('agg_expression_functions', () => { + describe('aggMin', () => { + const fn = functionWrapper(aggMin()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "min", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/min_fn.ts b/src/plugins/data/public/search/aggs/metrics/min_fn.ts new file mode 100644 index 0000000000000..b51da46a137b0 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/min_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggMin'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggMin = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.min.help', { + defaultMessage: 'Generates a serialized agg config for a Min agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.min.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.min.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.min.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.min.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.min.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.min.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MIN, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts index 94b9b1d8cd487..38a824629d304 100644 --- a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts @@ -22,8 +22,17 @@ import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsMovingAvg extends BaseAggParams { + buckets_path: string; + window?: number; + script?: string; + customMetric?: AggConfigSerialized; + metricAgg?: string; +} + export interface MovingAvgMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.test.ts new file mode 100644 index 0000000000000..d6c0e6b2cbd6e --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.test.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggMovingAvg } from './moving_avg_fn'; + +describe('agg_expression_functions', () => { + describe('aggMovingAvg', () => { + const fn = functionWrapper(aggMovingAvg()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + "script": undefined, + "window": undefined, + }, + "schema": undefined, + "type": "moving_avg", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + metricAgg: 'sum', + window: 10, + script: 'test', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": "sum", + "script": "test", + "window": 10, + }, + "schema": undefined, + "type": "moving_avg", + }, + } + `); + }); + + test('handles customMetric as a subexpression', () => { + const actual = fn({ + customMetric: fn({ buckets_path: 'the_sum' }), + buckets_path: 'the_sum', + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + "script": undefined, + "window": undefined, + }, + "schema": undefined, + "type": "moving_avg", + }, + "json": undefined, + "metricAgg": undefined, + "script": undefined, + "window": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + buckets_path: 'the_sum', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + buckets_path: 'the_sum', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts new file mode 100644 index 0000000000000..54a3fa176385b --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.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 { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggMovingAvg'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggMovingAvg = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.moving_avg.help', { + defaultMessage: 'Generates a serialized agg config for a Moving Average agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.moving_avg.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + metricAgg: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.metricAgg.help', { + defaultMessage: + 'Id for finding agg config to use for building parent pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.customMetric.help', { + defaultMessage: 'Agg config to use for building parent pipeline aggregations', + }), + }, + window: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.window.help', { + defaultMessage: 'The size of window to "slide" across the histogram.', + }), + }, + buckets_path: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.derivative.buckets_path.help', { + defaultMessage: 'Path to the metric of interest', + }), + }, + script: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.script.help', { + defaultMessage: + 'Id for finding agg config to use for building parent pipeline aggregations', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.moving_avg.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.MOVING_FN, + params: { + ...rest, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts index 0d79665ff9c4e..c8383f6bcc3d9 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts @@ -24,6 +24,12 @@ import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsPercentileRanks extends BaseAggParams { + field: string; + values?: number[]; +} // required by the values editor export type IPercentileRanksAggConfig = IResponseAggConfig; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.test.ts new file mode 100644 index 0000000000000..e3ce91bafd40a --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.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 { functionWrapper } from '../test_helpers'; +import { aggPercentileRanks } from './percentile_ranks_fn'; + +describe('agg_expression_functions', () => { + describe('aggPercentileRanks', () => { + const fn = functionWrapper(aggPercentileRanks()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "values": undefined, + }, + "schema": undefined, + "type": "percentile_ranks", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + values: [1, 2, 3], + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "values": Array [ + 1, + 2, + 3, + ], + }, + "schema": undefined, + "type": "percentile_ranks", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts new file mode 100644 index 0000000000000..851e938f28c1c --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts @@ -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 { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggPercentileRanks'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggPercentileRanks = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.percentile_ranks.help', { + defaultMessage: 'Generates a serialized agg config for a Percentile Ranks agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + values: { + types: ['number'], + multi: true, + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.values.help', { + defaultMessage: 'Range of percentiles ranks', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentile_ranks.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.PERCENTILE_RANKS, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.ts index 040a52588dd94..ad3c19cfaffcc 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.ts @@ -25,6 +25,12 @@ import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_respons import { getPercentileValue } from './percentiles_get_value'; import { ordinalSuffix } from './lib/ordinal_suffix'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsPercentiles extends BaseAggParams { + field: string; + percents?: number[]; +} export type IPercentileAggConfig = IResponseAggConfig; diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.test.ts new file mode 100644 index 0000000000000..2074cc1d89527 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.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 { functionWrapper } from '../test_helpers'; +import { aggPercentiles } from './percentiles_fn'; + +describe('agg_expression_functions', () => { + describe('aggPercentiles', () => { + const fn = functionWrapper(aggPercentiles()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "percents": undefined, + }, + "schema": undefined, + "type": "percentiles", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + percents: [1, 2, 3], + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "percents": Array [ + 1, + 2, + 3, + ], + }, + "schema": undefined, + "type": "percentiles", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts new file mode 100644 index 0000000000000..b799be07925fa --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts @@ -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 { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggPercentiles'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggPercentiles = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.percentiles.help', { + defaultMessage: 'Generates a serialized agg config for a Percentiles agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentiles.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.percentiles.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentiles.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.percentiles.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + percents: { + types: ['number'], + multi: true, + help: i18n.translate('data.search.aggs.metrics.percentiles.percents.help', { + defaultMessage: 'Range of percentiles ranks', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentiles.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.percentiles.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.PERCENTILES, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts index 2b1498560f862..fe112a50ad3c1 100644 --- a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts @@ -22,8 +22,15 @@ import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; import { GetInternalStartServicesFn } from '../../../types'; +export interface AggParamsSerialDiff extends BaseAggParams { + buckets_path: string; + customMetric?: AggConfigSerialized; + metricAgg?: string; +} + export interface SerialDiffMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.test.ts new file mode 100644 index 0000000000000..1bb859ad4bad8 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggSerialDiff } from './serial_diff_fn'; + +describe('agg_expression_functions', () => { + describe('aggSerialDiff', () => { + const fn = functionWrapper(aggSerialDiff()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "serial_diff", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + buckets_path: 'the_sum', + metricAgg: 'sum', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": "sum", + }, + "schema": undefined, + "type": "serial_diff", + }, + } + `); + }); + + test('handles customMetric as a subexpression', () => { + const actual = fn({ + customMetric: fn({ buckets_path: 'the_sum' }), + buckets_path: 'the_sum', + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "buckets_path": "the_sum", + "customLabel": undefined, + "customMetric": undefined, + "json": undefined, + "metricAgg": undefined, + }, + "schema": undefined, + "type": "serial_diff", + }, + "json": undefined, + "metricAgg": undefined, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + json: '{ "foo": true }', + buckets_path: 'the_sum', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + json: '/// intentionally malformed json ///', + buckets_path: 'the_sum', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts new file mode 100644 index 0000000000000..9ba313aff7386 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggSerialDiff'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggSerialDiff = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.serial_diff.help', { + defaultMessage: 'Generates a serialized agg config for a Serial Differencing agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.serial_diff.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + metricAgg: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.metricAgg.help', { + defaultMessage: + 'Id for finding agg config to use for building parent pipeline aggregations', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.customMetric.help', { + defaultMessage: 'Agg config to use for building parent pipeline aggregations', + }), + }, + buckets_path: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.serial_diff.buckets_path.help', { + defaultMessage: 'Path to the metric of interest', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.serial_diff.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.SERIAL_DIFF, + params: { + ...rest, + customMetric: args.customMetric?.value, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.ts index e972132542ceb..1733d5476f667 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.ts @@ -24,6 +24,11 @@ import { METRIC_TYPES } from './metric_agg_types'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsStdDeviation extends BaseAggParams { + field: string; +} interface ValProp { valProp: string[]; diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.test.ts new file mode 100644 index 0000000000000..bfa6aa7cc4122 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggStdDeviation } from './std_deviation_fn'; + +describe('agg_expression_functions', () => { + describe('aggStdDeviation', () => { + const fn = functionWrapper(aggStdDeviation()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "std_dev", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts new file mode 100644 index 0000000000000..70623e2e48041 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggStdDeviation'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggStdDeviation = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.std_deviation.help', { + defaultMessage: 'Generates a serialized agg config for a Standard Deviation agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.std_deviation.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.std_deviation.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.std_deviation.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.std_deviation.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.std_deviation.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.std_deviation.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.STD_DEV, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/sum.ts b/src/plugins/data/public/search/aggs/metrics/sum.ts index 545c6d6a4939e..70fc379f2d5f1 100644 --- a/src/plugins/data/public/search/aggs/metrics/sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/sum.ts @@ -22,11 +22,16 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; const sumTitle = i18n.translate('data.search.aggs.metrics.sumTitle', { defaultMessage: 'Sum', }); +export interface AggParamsSum extends BaseAggParams { + field: string; +} + export interface SumMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } diff --git a/src/plugins/data/public/search/aggs/metrics/sum_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/sum_fn.test.ts new file mode 100644 index 0000000000000..6e57632ba84cc --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/sum_fn.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggSum } from './sum_fn'; + +describe('agg_expression_functions', () => { + describe('aggSum', () => { + const fn = functionWrapper(aggSum()); + + test('required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + }, + "schema": undefined, + "type": "sum", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/sum_fn.ts new file mode 100644 index 0000000000000..a277aef02693f --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/sum_fn.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggSum'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggSum = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.sum.help', { + defaultMessage: 'Generates a serialized agg config for a Sum agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.sum.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.sum.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.sum.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.sum.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.sum.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.sum.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.SUM, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.ts index 15da2b485aee7..df7a76f151c07 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.ts @@ -23,6 +23,15 @@ import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; +import { BaseAggParams } from '../types'; + +export interface AggParamsTopHit extends BaseAggParams { + field: string; + aggregate: 'min' | 'max' | 'sum' | 'average' | 'concat'; + sortField?: string; + size?: number; + sortOrder?: 'desc' | 'asc'; +} export interface TopHitMetricAggDependencies { getInternalStartServices: GetInternalStartServicesFn; diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.test.ts b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.test.ts new file mode 100644 index 0000000000000..d0e9788f85025 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.test.ts @@ -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 { functionWrapper } from '../test_helpers'; +import { aggTopHit } from './top_hit_fn'; + +describe('agg_expression_functions', () => { + describe('aggTopHit', () => { + const fn = functionWrapper(aggTopHit()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + aggregate: 'min', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "aggregate": "min", + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": undefined, + "sortField": undefined, + "sortOrder": undefined, + }, + "schema": undefined, + "type": "top_hits", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + sortOrder: 'asc', + size: 6, + aggregate: 'min', + sortField: '_score', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "aggregate": "min", + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": 6, + "sortField": "_score", + "sortOrder": "asc", + }, + "schema": "whatever", + "type": "top_hits", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + aggregate: 'min', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + aggregate: 'min', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts new file mode 100644 index 0000000000000..adfd22b540e06 --- /dev/null +++ b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; + +const fnName = 'aggTopHit'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggTopHit = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.metrics.top_hit.help', { + defaultMessage: 'Generates a serialized agg config for a Top Hit agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.top_hit.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.top_hit.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.top_hit.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.top_hit.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + aggregate: { + types: ['string'], + required: true, + options: ['min', 'max', 'sum', 'average', 'concat'], + help: i18n.translate('data.search.aggs.metrics.top_hit.aggregate.help', { + defaultMessage: 'Aggregate type', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.top_hit.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + sortOrder: { + types: ['string'], + options: ['desc', 'asc'], + help: i18n.translate('data.search.aggs.metrics.top_hit.sortOrder.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + sortField: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.top_hit.sortField.help', { + defaultMessage: 'Field to order results by', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.top_hit.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.top_hit.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.TOP_HITS, + params: { + ...rest, + json: getParsedValue(args, 'json'), + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 8ad264f59cc27..a784bfaada4c7 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -30,12 +30,33 @@ import { AggParamsGeoTile, AggParamsGeoHash, AggParamsTerms, + AggParamsAvg, + AggParamsCardinality, + AggParamsGeoBounds, + AggParamsGeoCentroid, + AggParamsMax, + AggParamsMedian, + AggParamsMin, + AggParamsStdDeviation, + AggParamsSum, + AggParamsBucketAvg, + AggParamsBucketMax, + AggParamsBucketMin, + AggParamsBucketSum, + AggParamsCumulativeSum, + AggParamsDerivative, + AggParamsMovingAvg, + AggParamsPercentileRanks, + AggParamsPercentiles, + AggParamsSerialDiff, + AggParamsTopHit, AggParamsHistogram, AggParamsDateHistogram, AggTypesRegistrySetup, AggTypesRegistryStart, CreateAggConfigParams, getCalculateAutoTimeExpression, + METRIC_TYPES, BUCKET_TYPES, } from './'; @@ -102,4 +123,25 @@ export interface AggParamsMapping { [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; [BUCKET_TYPES.TERMS]: AggParamsTerms; + [METRIC_TYPES.AVG]: AggParamsAvg; + [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; + [METRIC_TYPES.COUNT]: BaseAggParams; + [METRIC_TYPES.GEO_BOUNDS]: AggParamsGeoBounds; + [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; + [METRIC_TYPES.MAX]: AggParamsMax; + [METRIC_TYPES.MEDIAN]: AggParamsMedian; + [METRIC_TYPES.MIN]: AggParamsMin; + [METRIC_TYPES.STD_DEV]: AggParamsStdDeviation; + [METRIC_TYPES.SUM]: AggParamsSum; + [METRIC_TYPES.AVG_BUCKET]: AggParamsBucketAvg; + [METRIC_TYPES.MAX_BUCKET]: AggParamsBucketMax; + [METRIC_TYPES.MIN_BUCKET]: AggParamsBucketMin; + [METRIC_TYPES.SUM_BUCKET]: AggParamsBucketSum; + [METRIC_TYPES.CUMULATIVE_SUM]: AggParamsCumulativeSum; + [METRIC_TYPES.DERIVATIVE]: AggParamsDerivative; + [METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg; + [METRIC_TYPES.PERCENTILE_RANKS]: AggParamsPercentileRanks; + [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; + [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; + [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 2b2d83c9f5a8b..546365b89d9be 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { Component } from 'react'; +import React from 'react'; import { debounce } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; @@ -39,7 +39,7 @@ export interface PhraseSuggestorState { * aggregatable), we pull out the common logic for requesting suggestions into this component * which both of them extend. */ -export class PhraseSuggestorUI extends Component< +export class PhraseSuggestorUI extends React.Component< T, PhraseSuggestorState > { diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 6ca1b7582001f..8ad1b5d392f3b 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -180,6 +180,7 @@ export function SavedQueryManagementComponent({ }} anchorPosition="downLeft" panelPaddingSize="none" + buffer={-8} ownFocus >

= ({ ); const ariaLabelWithoutTitle = i18n.translate( 'embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel', - { - defaultMessage: 'Panel options', - } + { defaultMessage: 'Panel options' } ); const button = ( diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index e61ad2a6eefed..84c6eea7c4ff1 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -22,6 +22,7 @@ import './index.scss'; import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; +export { EMBEDDABLE_ORIGINATING_APP_PARAM } from './types'; export { ACTION_ADD_PANEL, ACTION_APPLY_FILTER, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index fc5438b8c8dcb..196bd593eb8d5 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -22,10 +22,12 @@ import { Embeddable, EmbeddableInput } from '../embeddables'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; import { embeddablePluginMock } from '../../mocks'; +import { applicationServiceMock } from '../../../../../core/public/mocks'; const { doStart } = embeddablePluginMock.createInstance(); const start = doStart(); const getFactory = start.getEmbeddableFactory; +const applicationMock = applicationServiceMock.createStartContract(); class EditableEmbeddable extends Embeddable { public readonly type = 'EDITABLE_EMBEDDABLE'; @@ -41,7 +43,7 @@ class EditableEmbeddable extends Embeddable { } test('is compatible when edit url is available, in edit mode and editable', async () => { - const action = new EditPanelAction(getFactory, {} as any); + const action = new EditPanelAction(getFactory, applicationMock); expect( await action.isCompatible({ embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true), @@ -50,7 +52,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn }); test('getHref returns the edit urls', async () => { - const action = new EditPanelAction(getFactory, {} as any); + const action = new EditPanelAction(getFactory, applicationMock); expect(action.getHref).toBeDefined(); if (action.getHref) { @@ -64,7 +66,7 @@ test('getHref returns the edit urls', async () => { }); test('is not compatible when edit url is not available', async () => { - const action = new EditPanelAction(getFactory, {} as any); + const action = new EditPanelAction(getFactory, applicationMock); const embeddable = new ContactCardEmbeddable( { id: '123', @@ -83,7 +85,7 @@ test('is not compatible when edit url is not available', async () => { }); test('is not visible when edit url is available but in view mode', async () => { - const action = new EditPanelAction(getFactory, {} as any); + const action = new EditPanelAction(getFactory, applicationMock); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -98,7 +100,7 @@ test('is not visible when edit url is available but in view mode', async () => { }); test('is not compatible when edit url is available, in edit mode, but not editable', async () => { - const action = new EditPanelAction(getFactory, {} as any); + const action = new EditPanelAction(getFactory, applicationMock); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index d57867900c24b..d1edddb2aa86b 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -20,10 +20,11 @@ import { i18n } from '@kbn/i18n'; import { ApplicationStart } from 'kibana/public'; import { Action } from 'src/plugins/ui_actions/public'; +import { take } from 'rxjs/operators'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; -import { IEmbeddable } from '../embeddables'; import { EmbeddableStart } from '../../plugin'; +import { EMBEDDABLE_ORIGINATING_APP_PARAM, IEmbeddable } from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -35,11 +36,18 @@ export class EditPanelAction implements Action { public readonly type = ACTION_EDIT_PANEL; public readonly id = ACTION_EDIT_PANEL; public order = 50; + public currentAppId: string | undefined; constructor( private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'], private readonly application: ApplicationStart - ) {} + ) { + if (this.application?.currentAppId$) { + this.application.currentAppId$ + .pipe(take(1)) + .subscribe((appId: string | undefined) => (this.currentAppId = appId)); + } + } public getDisplayName({ embeddable }: ActionContext) { const factory = this.getEmbeddableFactory(embeddable.type); @@ -93,7 +101,15 @@ export class EditPanelAction implements Action { } public async getHref({ embeddable }: ActionContext): Promise { - const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined; + let editUrl = embeddable ? embeddable.getOutput().editUrl : undefined; + if (editUrl && this.currentAppId) { + editUrl += `?${EMBEDDABLE_ORIGINATING_APP_PARAM}=${this.currentAppId}`; + + // TODO: Remove this after https://github.com/elastic/kibana/pull/63443 + if (this.currentAppId === 'kibana') { + editUrl += `:${window.location.hash.split(/[\/\?]/)[1]}`; + } + } return editUrl ? editUrl : ''; } } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 9dd4c74c624d9..384297d8dee7d 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -44,6 +44,7 @@ import { import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { EuiBadge } from '@elastic/eui'; import { embeddablePluginMock } from '../../mocks'; +import { applicationServiceMock } from '../../../../../core/public/mocks'; const actionRegistry = new Map(); const triggerRegistry = new Map(); @@ -55,6 +56,7 @@ const trigger: Trigger = { id: CONTEXT_MENU_TRIGGER, }; const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); +const applicationMock = applicationServiceMock.createStartContract(); actionRegistry.set(editModeAction.id, editModeAction); triggerRegistry.set(trigger.id, trigger); @@ -159,7 +161,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => { getAllEmbeddableFactories={start.getEmbeddableFactories} getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} - application={{} as any} + application={applicationMock} overlays={{} as any} inspector={inspector} SavedObjectFinder={() => null} @@ -199,7 +201,7 @@ const renderInEditModeAndOpenContextMenu = async ( getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} - application={{} as any} + application={applicationMock} inspector={inspector} SavedObjectFinder={() => null} /> @@ -306,7 +308,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} - application={{} as any} + application={applicationMock} inspector={inspector} SavedObjectFinder={() => null} /> @@ -369,7 +371,7 @@ test('Updates when hidePanelTitles is toggled', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} - application={{} as any} + application={applicationMock} inspector={inspector} SavedObjectFinder={() => null} /> @@ -422,7 +424,7 @@ test('Check when hide header option is false', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} - application={{} as any} + application={applicationMock} inspector={inspector} SavedObjectFinder={() => null} hideHeader={false} diff --git a/src/plugins/embeddable/public/types.ts b/src/plugins/embeddable/public/types.ts index 2d112b2359818..a57af862f2a34 100644 --- a/src/plugins/embeddable/public/types.ts +++ b/src/plugins/embeddable/public/types.ts @@ -26,6 +26,8 @@ import { EmbeddableFactoryDefinition, } from './lib/embeddables'; +export const EMBEDDABLE_ORIGINATING_APP_PARAM = 'embeddableOriginatingApp'; + export type EmbeddableFactoryRegistry = Map; export type EmbeddableFactoryProvider = < diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 7176eef9bf413..64e2e7e4844cf 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -28,25 +28,12 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` >

- -

- -

-
@@ -102,25 +89,12 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` >

- -

- -

-
@@ -214,25 +188,12 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn >

- -

- -

-
@@ -326,25 +287,12 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn >

- -

- -

-
diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 8461b10aaa520..9ad5862896e8a 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -31,7 +31,6 @@ import { EuiSpacer, EuiFlexGroup, EuiFlexItem, - EuiText, EuiIcon, EuiPortal, } from '@elastic/eui'; @@ -141,20 +140,9 @@ export class Welcome extends React.Component {

- +

- -

- -

-
diff --git a/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 43e2af6d099e8..eab52795fefaa 100644 --- a/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -8,6 +8,7 @@ exports[`disableMsg 1`] = ` label="list control" > diff --git a/src/plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx index 6ded66917a3fd..cf95eed470beb 100644 --- a/src/plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/list_control.tsx @@ -114,6 +114,10 @@ class ListControlUi extends PureComponent
- -

- Elastic Kibana -

-

Exit full screen diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss index a2e951cb5b775..7548bd0c0db5f 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss +++ b/src/plugins/kibana_react/public/exit_full_screen_button/_exit_full_screen_button.scss @@ -10,12 +10,11 @@ bottom: $euiSizeS; position: fixed; display: block; - padding: 0; + padding: $euiSizeXS $euiSizeS; border: none; background: none; z-index: 5; background: $euiColorFullShade; - padding: $euiSizeXS; border-radius: $euiBorderRadius; text-align: left; @@ -28,11 +27,6 @@ } } -.dshExitFullScreenButton__title { - line-height: 1.2; - color: $euiColorEmptyShade; -} - .dshExitFullScreenButton__text { line-height: 1.2; color: makeHighContrastColor($euiColorMediumShade, $euiColorFullShade); diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx index 97fc02ac64e12..8e3113aa9ccfd 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React, { PureComponent } from 'react'; import { EuiScreenReaderOnly, keyCodes } from '@elastic/eui'; -import { EuiIcon, EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; export interface ExitFullScreenButtonProps { onExitFullScreenMode: () => void; @@ -69,17 +69,7 @@ class ExitFullScreenButtonUi extends PureComponent {

- -

- {i18n.translate( - 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonTitle', - { - defaultMessage: 'Elastic Kibana', - } - )} -

-
- +

{i18n.translate( 'kibana-react.exitFullScreenButton.exitFullScreenModeButtonText', diff --git a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index ce583236e7c81..8187e70b1bbd1 100644 --- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - -import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import React, { Component, createRef } from 'react'; import { EuiFormRow, EuiDualRange } from '@elastic/eui'; import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; @@ -35,8 +35,8 @@ interface Props extends Omit void; - min?: ValueMember; - max?: ValueMember; + min?: number; + max?: number; } interface State { @@ -72,6 +72,18 @@ export class ValidatedDualRange extends Component { return null; } + // Can remove after eui#3412 is resolved + componentDidMount() { + if (this.trackRef.current) { + const track = this.trackRef.current.querySelector('.euiRangeTrack'); + if (track) { + track.setAttribute('aria-hidden', 'true'); + } + } + } + + trackRef = createRef(); + // @ts-ignore state populated by getDerivedStateFromProps state: State = {}; @@ -103,29 +115,38 @@ export class ValidatedDualRange extends Component { value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars - // @ts-ignore ...rest // TODO: Consider alternatives for spread operator in component } = this.props; return ( - - + - + isInvalid={!this.state.isValid} + error={this.state.errorMessage ? [this.state.errorMessage] : []} + label={label} + display={formRowDisplay} + > + + +

); } } diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index 63d919377f89e..aae58ba3e4651 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,7 +27,7 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), }, capabilities diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 38db1039042e5..843bbfde654ee 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,7 +64,7 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), href: '#/management', }, diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 1c9e1d5c89550..df2398412dac2 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -36,8 +36,8 @@ export class ManagementPlugin implements Plugin } title={ - } diff --git a/src/plugins/newsfeed/public/components/loading_news.tsx b/src/plugins/newsfeed/public/components/loading_news.tsx index fcbc7970377d4..d95577878cd7a 100644 --- a/src/plugins/newsfeed/public/components/loading_news.tsx +++ b/src/plugins/newsfeed/public/components/loading_news.tsx @@ -20,12 +20,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt } from '@elastic/eui'; -import { EuiLoadingKibana } from '@elastic/eui'; +import { EuiLoadingElastic } from '@elastic/eui'; export const NewsLoadingPrompt = () => { return ( } + title={} body={

void; title: string; showCopyOnSave: boolean; + initialCopyOnSave?: boolean; objectType: string; confirmButtonLabel?: React.ReactNode; - options?: React.ReactNode; + options?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); description?: string; showDescription: boolean; } -interface State { +export interface SaveModalState { title: string; copyOnSave: boolean; isTitleDuplicateConfirmed: boolean; @@ -71,11 +72,11 @@ interface State { const generateId = htmlIdGenerator(); -export class SavedObjectSaveModal extends React.Component { +export class SavedObjectSaveModal extends React.Component { private warning = React.createRef(); public readonly state = { title: this.props.title, - copyOnSave: false, + copyOnSave: Boolean(this.props.initialCopyOnSave), isTitleDuplicateConfirmed: false, hasTitleDuplicate: false, isLoading: false, @@ -139,7 +140,9 @@ export class SavedObjectSaveModal extends React.Component { {this.renderViewDescription()} - {this.props.options} + {typeof this.props.options === 'function' + ? this.props.options(this.state) + : this.props.options} diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx new file mode 100644 index 0000000000000..34f4bc593fdc4 --- /dev/null +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { OnSaveProps, SaveModalState, SavedObjectSaveModal } from '.'; + +interface SaveModalDocumentInfo { + id?: string; + title: string; + description?: string; +} + +interface OriginSaveModalProps { + originatingApp?: string; + documentInfo: SaveModalDocumentInfo; + objectType: string; + onClose: () => void; + onSave: (props: OnSaveProps & { returnToOrigin: boolean }) => void; +} + +export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) { + const [returnToOriginMode, setReturnToOriginMode] = useState(Boolean(props.originatingApp)); + const { documentInfo } = props; + + const returnLabel = i18n.translate('savedObjects.saveModalOrigin.returnToOriginLabel', { + defaultMessage: 'Return', + }); + const addLabel = i18n.translate('savedObjects.saveModalOrigin.addToOriginLabel', { + defaultMessage: 'Add', + }); + + const getReturnToOriginSwitch = (state: SaveModalState) => { + if (!props.originatingApp) { + return; + } + let origin = props.originatingApp!; + + // TODO: Remove this after https://github.com/elastic/kibana/pull/63443 + if (origin.startsWith('kibana:')) { + origin = origin.split(':')[1]; + } + + if ( + !state.copyOnSave || + origin === 'dashboard' // dashboard supports adding a copied panel on save... + ) { + const originVerb = !documentInfo.id || state.copyOnSave ? addLabel : returnLabel; + return ( + + + { + setReturnToOriginMode(event.target.checked); + }} + label={ + + } + /> + + + ); + } else { + setReturnToOriginMode(false); + } + }; + + const onModalSave = (onSaveProps: OnSaveProps) => { + props.onSave({ ...onSaveProps, returnToOrigin: returnToOriginMode }); + }; + + const confirmButtonLabel = returnToOriginMode + ? i18n.translate('savedObjects.saveModalOrigin.saveAndReturnLabel', { + defaultMessage: 'Save and return', + }) + : null; + + return ( + + ); +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 4721d166c65e5..b7bd9368e8b1c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -2,11 +2,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 8c0117e5a7266..c392d8ce64205 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -489,4 +489,4 @@ exports[`TelemetryManagementSectionComponent renders null because query does not /> `; -exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `""`; +exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `null`; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index d0c2bd13f802d..c13f639f31447 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -18,10 +18,9 @@ */ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { TelemetryManagementSection } from './telemetry_management_section'; +import TelemetryManagementSection from './telemetry_management_section'; import { TelemetryService } from '../../../telemetry/public/services'; import { coreMock } from '../../../../core/public/mocks'; -import { telemetryManagementSectionWrapper } from './telemetry_management_section_wrapper'; describe('TelemetryManagementSectionComponent', () => { const coreStart = coreMock.createStart(); @@ -270,10 +269,12 @@ describe('TelemetryManagementSectionComponent', () => { notifications: coreStart.notifications, http: coreSetup.http, }); - const Wrapper = telemetryManagementSectionWrapper(telemetryService); + expect( shallowWithIntl( - { }); }; } + +// required for lazy loading +// eslint-disable-next-line import/no-default-export +export default TelemetryManagementSection; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx index b8b20b68f666e..f61268c4772a3 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx @@ -17,23 +17,27 @@ * under the License. */ -import React from 'react'; +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; -import { TelemetryManagementSection } from './telemetry_management_section'; // It should be this but the types are way too vague in the AdvancedSettings plugin `Record` // type Props = Omit; type Props = any; +const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section')); + export function telemetryManagementSectionWrapper( telemetryService: TelemetryPluginSetup['telemetryService'] ) { const TelemetryManagementSectionWrapper = (props: Props) => ( - + }> + + ); return TelemetryManagementSectionWrapper; diff --git a/src/plugins/telemetry_management_section/public/plugin.ts b/src/plugins/telemetry_management_section/public/plugin.tsx similarity index 100% rename from src/plugins/telemetry_management_section/public/plugin.ts rename to src/plugins/telemetry_management_section/public/plugin.tsx diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index 4cbc4dd2a053c..e3504c7c5d301 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -17,6 +17,8 @@ * under the License. */ +// @ts-ignore +import React from 'react'; import { Action, ActionContext as Context, ActionDefinition } from './action'; import { Presentable } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js index 0cd501e2d0344..6b5796d6eb5bc 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -258,7 +258,7 @@ function addPercentageCol(columns, title, rows, insertAtIndex) { formatter, }); const newRows = rows.map(row => ({ - [newId]: formatter.convert(row[id] / sumTotal / 100), + [newId]: row[id] / sumTotal, ...row, })); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 40e89008e7562..435ec9027eef2 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -25,7 +25,7 @@ import { PluginInitializerContext, RecursiveReadonly, } from '../../../../src/core/server'; -import { deepFreeze } from '../../../../src/core/utils'; +import { deepFreeze } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index afaea0d9b8462..43fc2671ba42a 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -17,7 +17,6 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` class="euiTextColor euiTextColor--subdued" >