diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 97099c6f87448..63ee2ca48659d 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -37,22 +37,31 @@ pipeline { deleteDir() gitCheckout(basedir: "${BASE_DIR}", githubNotifyFirstTimeContributor: false, shallow: false, reference: "/var/lib/jenkins/.git-references/kibana.git") + + // Filter when to run based on the below reasons: + // - On a PRs when: + // - There are changes related to the APM UI project + // - only when the owners of those changes are members of the given GitHub teams + // - On merges to branches when: + // - There are changes related to the APM UI project + // - FORCE parameter is set to true. script { + def apm_updated = false dir("${BASE_DIR}"){ - def regexps =[ "^x-pack/plugins/apm/.*" ] - env.APM_UPDATED = isGitRegionMatch(patterns: regexps) + apm_updated = isGitRegionMatch(patterns: [ "^x-pack/plugins/apm/.*" ]) + } + if (isPR()) { + def isMember = isMemberOf(user: env.CHANGE_AUTHOR, team: ['apm-ui', 'uptime']) + setEnvVar('RUN_APM_E2E', params.FORCE || (apm_updated && isMember)) + } else { + setEnvVar('RUN_APM_E2E', params.FORCE || apm_updated) } } } } stage('Prepare Kibana') { options { skipDefaultCheckout() } - when { - anyOf { - expression { return params.FORCE } - expression { return env.APM_UPDATED != "false" } - } - } + when { expression { return env.RUN_APM_E2E != "false" } } environment { JENKINS_NODE_COOKIE = 'dontKillMe' } @@ -70,12 +79,7 @@ pipeline { } stage('Smoke Tests'){ options { skipDefaultCheckout() } - when { - anyOf { - expression { return params.FORCE } - expression { return env.APM_UPDATED != "false" } - } - } + when { expression { return env.RUN_APM_E2E != "false" } } steps{ notifyTestStatus('Running smoke tests', 'PENDING') dir("${BASE_DIR}"){ diff --git a/docs/developer/best-practices/images/state_inside_the_link.png b/docs/developer/best-practices/images/state_inside_the_link.png new file mode 100644 index 0000000000000..833478ccbda68 Binary files /dev/null and b/docs/developer/best-practices/images/state_inside_the_link.png differ diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 42cee6ef0e58a..13ea010d0aa96 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -122,6 +122,14 @@ In addition, if users are relying on state stored in your app’s URL as part of your public contract, keep in mind that you may also need to provide backwards compatibility for bookmarked URLs. +[discrete] +=== Routing, Navigation and URL + +The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation. +Some of that tooling is inside `core`, some is available as part of various plugins. + +<> to get an idea of available tools and common approaches for handling routing and browser navigation. + [discrete] === Testing & stability @@ -131,6 +139,8 @@ Review: * <> * <> +include::navigation.asciidoc[leveloffset=+1] + include::stability.asciidoc[leveloffset=+1] include::security.asciidoc[leveloffset=+1] diff --git a/docs/developer/best-practices/navigation.asciidoc b/docs/developer/best-practices/navigation.asciidoc new file mode 100644 index 0000000000000..d01f2c2aa0f95 --- /dev/null +++ b/docs/developer/best-practices/navigation.asciidoc @@ -0,0 +1,226 @@ +[[kibana-navigation]] +== Routing, Navigation and URL + +The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation. +Some of that tooling is inside `core`, some is available as part of various plugins. + +The purpose of this guide is to give a high-level overview of available tools and to explain common approaches for handling routing and browser navigation. + +This guide covers following topics: + +* <> +* <> +* <> +* <> +* <> +* <> + +[[deep-linking]] +=== Deep-linking into {kib} apps + +Assuming you want to link from your app to *Discover*. When building such URL there are two things to consider: + +1. Prepending a proper `basePath`. +2. Specifying *Discover* state. + +==== Prepending a proper `basePath` + +To prepend {kib}'s `basePath` use {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.ibasepath.prepend.md[core.http.basePath.prepend] helper: + +[source,typescript jsx] +---- +const discoverUrl = core.http.basePath.prepend(`/discover`); + +console.log(discoverUrl); // http://localhost:5601/bpr/s/space/app/discover +---- + +==== Specifying state + +**Consider a {kib} app URL a part of app's plugin contract:** + +. Avoid hardcoding other app's URL in your app's code. +. Avoid generating other app's state and serializing it into URL query params. + +[source,typescript jsx] +---- +// Avoid relying on other app's state structure in your app's code: +const discoverUrlWithSomeState = core.http.basePath.prepend(`/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2020-09-10T11:39:50.203Z',to:'2020-09-10T11:40:20.249Z'))&_a=(columns:!(_source),filters:!(),index:'90943e30-9a47-11e8-b64d-95841ca0b247',interval:auto,query:(language:kuery,query:''),sort:!())`); +---- + +Instead, each app should expose {kib-repo}tree/{branch}/src/plugins/share/public/url_generators/README.md[a URL generator]. +Other apps should use those URL generators for creating URLs. + +[source,typescript jsx] +---- +// Properly generated URL to *Discover* app. Generator code is owned by *Discover* app and available on *Discover*'s plugin contract. +const discoverUrl = discoverUrlGenerator.createUrl({filters, timeRange}); +---- + +To get a better idea, take a look at *Discover* URL generator {kib-repo}tree/{branch}/src/plugins/discover/public/url_generator.ts[implementation]. +It allows specifying various **Discover** app state pieces like: index pattern, filters, query, time range and more. + +There are two ways to access other's app URL generator in your code: + +1. From a plugin contract of a destination app *(preferred)*. +2. Using URL generator service instance on `share` plugin contract (in case an explicit plugin dependency is not possible). + +In case you want other apps to link to your app, then you should create a URL generator and expose it on your plugin's contract. + + +[[navigating-between-kibana-apps]] +=== Navigating between {kib} apps + +{kib} is a single page application and there is a set of simple rules developers should follow +to make sure there is no page reload when navigating from one place in {kib} to another. + +For example, navigation using native browser APIs would cause a full page reload. + +[source,js] +---- +const urlToADashboard = core.http.basePath.prepend(`/dashboard/my-dashboard`); + +// this would cause a full page reload: +window.location.href = urlToADashboard; +---- + +To navigate between different {kib} apps without a page reload there are APIs in `core`: + +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md[core.application.navigateToApp] +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md[core.application.navigateToUrl] + +*Rendering a link to a different {kib} app on its own would also cause a full page reload:* + +[source,typescript jsx] +---- +const myLink = () => + Go to Dashboard; +---- + +A workaround could be to handle a click, prevent browser navigation and use `core.application.navigateToApp` API: + +[source,typescript jsx] +---- +const MySPALink = () => + { + e.preventDefault(); + core.application.navigateToApp('dashboard', { path: '/my-dashboard' }); + }} + > + Go to Dashboard + ; +---- + +As it would be too much boilerplate to do this for each {kib} link in your app, there is a handy wrapper that helps with it: +{kib-repo}tree/{branch}/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx#L49[RedirectAppLinks]. + +[source,typescript jsx] +---- +const MyApp = () => + + {/*...*/} + {/* navigations using this link will happen in SPA friendly way */} + Go to Dashboard + {/*...*/} + +---- + +[[routing]] +=== Setting up internal app routing + +It is very common for {kib} apps to use React and React Router. +Common rules to follow in this scenario: + +* Set up `BrowserRouter` and not `HashRouter`. +* *Initialize your router with `history` instance provided by the `core`.* + +This is required to make sure `core` is aware of navigations triggered inside your app, so it could act accordingly when needed. + +* `Core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] instance. +* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md[Example usage] +* {kib-repo}tree/{branch}/test/plugin_functional/plugins/core_plugin_a/public/application.tsx#L120[Example plugin] + +Relative links will be resolved relative to your app's route (e.g.: `http://localhost5601/app/{your-app-id}`) +and setting up internal links in your app in SPA friendly way would look something like: + +[source,typescript jsx] +---- +import {Link} from 'react-router-dom'; + +const MyInternalLink = () => +---- + +[[history-and-location]] +=== Using history and browser location + +Try to avoid using `window.location` and `window.history` directly. + +Instead, consider using {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] +instance provided by `core`. + +* This way `core` will know about location changes triggered within your app, and it would act accordingly. +* Some plugins are listening to location changes. Triggering location change manually could lead to unpredictable and hard-to-catch bugs. + +Common use-case for using +`core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] directly: + +* Reading/writing query params or hash. +* Imperatively triggering internal navigations within your app. +* Listening to browser location changes. + + +[[state-sync]] +=== Syncing state with URL + +Historically {kib} apps store _a lot_ of application state in the URL. +The most common pattern that {kib} apps follow today is storing state in `_a` and `_g` query params in https://github.com/w33ble/rison-node#readme[rison] format. +[[query-params]] +Those query params follow the convention: + +* `_g` (*global*) - global UI state that should be shared and synced across multiple apps. common example from Analyze group apps: time range, refresh interval, *pinned* filters. +* `_a` (*application*) - UI state scoped to current app. + +NOTE: After migrating to KP platform we got navigations without page reloads. Since then there is no real need to follow `_g` and `_a` separation anymore. It's up you to decide if you want to follow this pattern or if you prefer a single query param or something else. The need for this separation earlier is explained in <>. + +There are utils to help you to implement such kind of state syncing. + +**When you should consider using state syncing utils:** + +* You want to sync your application state with URL in similar manner Analyze group applications do. +* You want to follow platform's <> out of the box. +* You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box. +* You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`. +* In case you want to sync part of your state with URL, but other part of it with browser storage. + +**When you shouldn't use state syncing utils:** + +* Adding a query param flag or simple key/value to the URL. + +Follow {kib-repo}tree/{branch}/src/plugins/kibana_utils/docs/state_sync#state-syncing-utilities[these] docs to learn more. + + +[[preserve-state]] +=== Preserving state between navigations + +Consider the scenario: + +1. You are in *Dashboard* app looking at a dashboard with some filters applied; +2. Navigate to *Discover* using in-app navigation; +3. Change the time filter' +4. Navigate to *Dashboard* using in-app navigation. + +You'd notice that you were navigated to *Dashboard* app with the *same state* that you left it with, +except that the time filter has changed to the one you applied on *Discover* app. + +Historically {kib} Analyze groups apps achieve that behavior relying on state in the URL. +If you'd have a closer look on a link in the navigation, +you'd notice that state is stored inside that link, and it also gets updated whenever relevant state changes happen: + +[role="screenshot"] +image:images/state_inside_the_link.png[State is stored inside the navigation link] + +This is where <> into `_a` and `_g` query params comes into play. What is considered a *global* state gets constantly updated in those navigation links. In the example above it was a time filter. +This is backed by {kib-repo}tree/{branch}/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts#L57[KbnUrlTracker] util. You can use it to achieve similar behavior. + +NOTE: After migrating to KP navigation works without page reloads and all plugins are loaded simultaneously. +Hence, likely there are simpler ways to preserve state of your application, unless you want to do it through URL. diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index a36363d22d87d..84df05154fb63 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -57,7 +57,7 @@ export interface AttributeServiceOptions { type: string, attributes: A, savedObjectId?: string - ) => Promise<{ id: string }>; + ) => Promise<{ id?: string } | { error: Error }>; customUnwrapMethod?: (savedObject: SimpleSavedObject) => A; } @@ -124,7 +124,10 @@ export class AttributeService< newAttributes, savedObjectId ); - return { ...originalInput, savedObjectId: savedItem.id } as RefType; + if ('id' in savedItem) { + return { ...originalInput, savedObjectId: savedItem.id } as RefType; + } + return { ...originalInput } as RefType; } if (savedObjectId) { @@ -208,7 +211,6 @@ export class AttributeService< return { error }; } }; - if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { this.showSaveModal( ; + +const createStartContract = (): DashboardStart => { + // @ts-ignore + const startContract: DashboardStart = { + getAttributeService: jest.fn(), + }; + + return startContract; +}; + +export const dashboardPluginMock = { + createStartContract, +}; diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index 570a78fc41ea9..b22f16c94aff8 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -37,11 +37,11 @@ export const defaultEmbeddableFactoryProvider = < getExplicitInput: def.getExplicitInput ? def.getExplicitInput.bind(def) : () => Promise.resolve({}), - createFromSavedObject: - def.createFromSavedObject ?? - ((savedObjectId: string, input: Partial, parent?: IContainer) => { - throw new Error(`Creation from saved object not supported by type ${def.type}`); - }), + createFromSavedObject: def.createFromSavedObject + ? def.createFromSavedObject.bind(def) + : (savedObjectId: string, input: Partial, parent?: IContainer) => { + throw new Error(`Creation from saved object not supported by type ${def.type}`); + }, create: def.create.bind(def), type: def.type, isEditable: def.isEditable.bind(def), diff --git a/src/plugins/expressions/common/expression_types/specs/boolean.ts b/src/plugins/expressions/common/expression_types/specs/boolean.ts index adbdeafc34fd2..73b0b98eaaf06 100644 --- a/src/plugins/expressions/common/expression_types/specs/boolean.ts +++ b/src/plugins/expressions/common/expression_types/specs/boolean.ts @@ -41,7 +41,6 @@ export const boolean: ExpressionTypeDefinition<'boolean', boolean> = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: name } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index dd3c653878de7..c201e99faeb03 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -52,7 +52,10 @@ export type DatatableRow = Record; export interface DatatableColumnMeta { type: DatatableColumnType; field?: string; + index?: string; params?: SerializableState; + source?: string; + sourceParams?: SerializableState; } /** * This type represents the shape of a column in a `Datatable`. @@ -63,17 +66,11 @@ export interface DatatableColumn { meta: DatatableColumnMeta; } -export interface DatatableMeta { - type?: string; - source?: string; -} - /** * A `Datatable` in Canvas is a unique structure that represents tabulated data. */ export interface Datatable { type: typeof name; - meta?: DatatableMeta; columns: DatatableColumn[]; rows: DatatableRow[]; } diff --git a/src/plugins/expressions/common/expression_types/specs/num.ts b/src/plugins/expressions/common/expression_types/specs/num.ts index 041747f39740b..d208a9dcf73c8 100644 --- a/src/plugins/expressions/common/expression_types/specs/num.ts +++ b/src/plugins/expressions/common/expression_types/specs/num.ts @@ -73,7 +73,6 @@ export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = { }, datatable: ({ value }): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/number.ts b/src/plugins/expressions/common/expression_types/specs/number.ts index c5fdacf3408a1..c30d3fe943d42 100644 --- a/src/plugins/expressions/common/expression_types/specs/number.ts +++ b/src/plugins/expressions/common/expression_types/specs/number.ts @@ -55,7 +55,6 @@ export const number: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/string.ts b/src/plugins/expressions/common/expression_types/specs/string.ts index 3d52707279bfc..0869e21e455f7 100644 --- a/src/plugins/expressions/common/expression_types/specs/string.ts +++ b/src/plugins/expressions/common/expression_types/specs/string.ts @@ -40,7 +40,6 @@ export const string: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'string' } }], rows: [{ value }], }), diff --git a/src/plugins/kibana_react/README.md b/src/plugins/kibana_react/README.md index 3389af9f1800b..adbdb628ea9dd 100644 --- a/src/plugins/kibana_react/README.md +++ b/src/plugins/kibana_react/README.md @@ -2,7 +2,6 @@ Tools for building React applications in Kibana. - ## Context You can create React context that holds Core or plugin services that your plugin depends on. @@ -51,7 +50,6 @@ import { KibanaContextProvider } from 'kibana-react'; ``` - ## Accessing context Using `useKibana` hook. @@ -61,11 +59,7 @@ import { useKibana } from 'kibana-react'; const Demo = () => { const kibana = useKibana(); - return ( -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- ); + return
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
; }; ``` @@ -75,11 +69,7 @@ Using `withKibana()` higher order component. import { withKibana } from 'kibana-react'; const Demo = ({ kibana }) => { - return ( -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- ); + return
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
; }; export default withKibana(Demo); @@ -92,21 +82,17 @@ import { UseKibana } from 'kibana-react'; const Demo = () => { return ( - {kibana => -
- {kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'} -
- }
+ + {(kibana) =>
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
} +
); }; ``` - ## `uiSettings` service Wrappers around Core's `uiSettings` service. - ### `useUiSetting` hook `useUiSetting` synchronously returns the latest setting from `CoreStart['uiSettings']` service. @@ -116,11 +102,7 @@ import { useUiSetting } from 'kibana-react'; const Demo = () => { const darkMode = useUiSetting('theme:darkMode'); - return ( -
- {darkMode ? 'dark' : 'light'} -
- ); + return
{darkMode ? 'dark' : 'light'}
; }; ``` @@ -130,7 +112,6 @@ const Demo = () => { useUiSetting(key: string, defaultValue: T): T; ``` - ### `useUiSetting$` hook `useUiSetting$` synchronously returns the latest setting from `CoreStart['uiSettings']` service and @@ -141,11 +122,7 @@ import { useUiSetting$ } from 'kibana-react'; const Demo = () => { const [darkMode] = useUiSetting$('theme:darkMode'); - return ( -
- {darkMode ? 'dark' : 'light'} -
- ); + return
{darkMode ? 'dark' : 'light'}
; }; ``` @@ -155,7 +132,6 @@ const Demo = () => { useUiSetting$(key: string, defaultValue: T): [T, (newValue: T) => void]; ``` - ## `overlays` service Wrapper around Core's `overlays` service, allows you to display React modals and flyouts @@ -166,13 +142,11 @@ import { createKibanaReactContext } from 'kibana-react'; class MyPlugin { start(core) { - const { value: { overlays } } = createKibanaReactContext(core); + const { + value: { overlays }, + } = createKibanaReactContext(core); - overlays.openModal( -
- Hello world! -
- ); + overlays.openModal(
Hello world!
); } } ``` @@ -186,16 +160,11 @@ You can access `overlays` service through React context. const Demo = () => { const { overlays } = useKibana(); useEffect(() => { - overlays.openModal( -
- Oooops! {errorMessage} -
- ); + overlays.openModal(
Oooops! {errorMessage}
); }, [errorMessage]); }; ``` - ## `notifications` service Wrapper around Core's `notifications` service, allows you to render React elements @@ -206,11 +175,13 @@ import { createKibanaReactContext } from 'kibana-react'; class MyPlugin { start(core) { - const { value: { notifications } } = createKibanaReactContext(core); + const { + value: { notifications }, + } = createKibanaReactContext(core); notifications.toasts.show({ title:
Hello
, - body:
world!
+ body:
world!
, }); } } @@ -234,3 +205,15 @@ const Demo = () => { }, [errorMessage]); }; ``` + +## RedirectAppLinks + +Utility component that will intercept click events on children anchor (`
`) elements to call +`application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation +when the link points to a valid Kibana app. + +```tsx + + Go to another-app + +``` diff --git a/src/plugins/kibana_utils/common/state_containers/README.md b/src/plugins/kibana_utils/common/state_containers/README.md new file mode 100644 index 0000000000000..c623e8b306438 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_containers/README.md @@ -0,0 +1,2 @@ +* [docs](../../docs/state_containers) +* [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) \ No newline at end of file diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md index c84bf7f236330..6b4eb0cb1749b 100644 --- a/src/plugins/kibana_utils/docs/state_sync/README.md +++ b/src/plugins/kibana_utils/docs/state_sync/README.md @@ -3,6 +3,18 @@ State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. +**When you should consider using state syncing utils:** + +- You want to sync your application state with URL in similar manner analyze applications do that. +- You want to follow platform's <> out of the box. +- You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box. +- You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`. +- In case you want to sync part of your state with URL, but other part of it with browser storage. + +**When you shouldn't look into using state syncing utils:** + +- Adding a query param flag or simple key/value to URL + They are designed to work together with [state containers](../state_containers). But state containers are not required. State syncing utilities include: @@ -42,9 +54,9 @@ stateContainer.set({ count: 2 }); stop(); ``` -## Demos Plugins +## Demo Plugins -See demos plugins [here](../../../../../examples/state_containers_examples). +See demo plugins [here](../../../../../examples/state_containers_examples). To run them, start kibana with `--run-examples` flag. diff --git a/src/plugins/kibana_utils/public/state_sync/README.md b/src/plugins/kibana_utils/public/state_sync/README.md new file mode 100644 index 0000000000000..eb5f6e60958fc --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/README.md @@ -0,0 +1,3 @@ +- [docs](../../docs/state_sync) +- [demo plugins](../../../../../examples/state_containers_examples): run Kibana with `--run-examples` flag. +- [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index 707ccd1e55bb0..13874166bc558 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -18,22 +18,27 @@ */ const filter = (metric) => metric.type === 'filter_ratio'; +import { esQuery } from '../../../../../../data/server'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; -export function ratios(req, panel) { +export function ratios(req, panel, esQueryConfig, indexPatternObject) { return (next) => (doc) => { panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); if (column.metrics.some(filter)) { column.metrics.filter(filter).forEach((metric) => { - overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, { - query_string: { query: metric.numerator || '*', analyze_wildcard: true }, - }); - overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, { - query_string: { query: metric.denominator || '*', analyze_wildcard: true }, - }); + overwrite( + doc, + `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, + esQuery.buildEsQuery(indexPatternObject, metric.numerator, [], esQueryConfig) + ); + overwrite( + doc, + `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, + esQuery.buildEsQuery(indexPatternObject, metric.denominator, [], esQueryConfig) + ); let numeratorPath = `${metric.id}-numerator>_count`; let denominatorPath = `${metric.id}-denominator>_count`; @@ -46,7 +51,7 @@ export function ratios(req, panel) { }), }; overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.aggs`, aggBody); - overwrite(doc, `${aggBody}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); numeratorPath = `${metric.id}-numerator>metric`; denominatorPath = `${metric.id}-denominator>metric`; } diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index da3edfbdd3bf5..0bd5de1d9ee15 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"], + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector", "dashboard"], "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 194deef82a5f0..b27d24d980e8d 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -18,7 +18,13 @@ */ import { Vis } from '../types'; -import { VisualizeInput, VisualizeEmbeddable } from './visualize_embeddable'; +import { + VisualizeInput, + VisualizeEmbeddable, + VisualizeByValueInput, + VisualizeByReferenceInput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { @@ -30,10 +36,18 @@ import { } from '../services'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { AttributeService } from '../../../dashboard/public'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, input: Partial & { id: string }, + savedVisualizationsLoader?: SavedVisualizationsLoader, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, parent?: IContainer ): Promise => { const savedVisualizations = getSavedVisualizationsLoader(); @@ -55,6 +69,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const indexPattern = vis.data.indexPattern; const indexPatterns = indexPattern ? [indexPattern] : []; const editable = getCapabilities().visualize.save as boolean; + return new VisualizeEmbeddable( getTimeFilter(), { @@ -66,6 +81,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe deps, }, input, + attributeService, + savedVisualizationsLoader, parent ); } catch (e) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index cc278a6ee9b3d..18ae68ec40fe5 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -20,6 +20,7 @@ import _, { get } from 'lodash'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -35,6 +36,8 @@ import { Embeddable, IContainer, Adapters, + SavedObjectEmbeddableInput, + ReferenceOrValueEmbeddable, } from '../../../../plugins/embeddable/public'; import { IExpressionLoaderParams, @@ -47,6 +50,10 @@ import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { TriggerId } from '../../../ui_actions/public'; +import { SavedObjectAttributes } from '../../../../core/types'; +import { AttributeService } from '../../../dashboard/public'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -75,9 +82,19 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } +export type VisualizeSavedObjectAttributes = SavedObjectAttributes & { + title: string; + vis?: Vis; + savedVis?: VisSavedObject; +}; +export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput; +export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput; + type ExpressionLoader = InstanceType; -export class VisualizeEmbeddable extends Embeddable { +export class VisualizeEmbeddable + extends Embeddable + implements ReferenceOrValueEmbeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; private timeRange?: TimeRange; @@ -93,11 +110,23 @@ export class VisualizeEmbeddable extends Embeddable; + private savedVisualizationsLoader?: SavedVisualizationsLoader; constructor( timefilter: TimefilterContract, { vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration, initialInput: VisualizeInput, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, + savedVisualizationsLoader?: SavedVisualizationsLoader, parent?: IContainer ) { super( @@ -118,6 +147,8 @@ export class VisualizeEmbeddable extends Embeddable { + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + return this.attributeService.inputIsRefType(input as VisualizeByReferenceInput); + }; + + getInputAsValueType = async (): Promise => { + const input = { + savedVis: this.vis.serialize(), + }; + if (this.getTitle()) { + input.savedVis.title = this.getTitle(); + } + delete input.savedVis.id; + return new Promise((resolve) => { + resolve({ ...(input as VisualizeByValueInput) }); + }); + }; + + getInputAsRefType = async (): Promise => { + const savedVis = await this.savedVisualizationsLoader?.get({}); + if (!savedVis) { + throw new Error('Error creating a saved vis object'); + } + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + const saveModalTitle = this.getTitle() + ? this.getTitle() + : i18n.translate('visualizations.embeddable.placeholderTitle', { + defaultMessage: 'Placeholder Title', + }); + // @ts-ignore + const attributes: VisualizeSavedObjectAttributes = { + savedVis, + vis: this.vis, + title: this.vis.title, + }; + return this.attributeService.getInputAsRefType( + { + id: this.id, + attributes, + }, + { showSaveModal: true, saveModalTitle } + ); + }; } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index b81ff5c166183..75e53e8e92dbe 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -28,7 +28,14 @@ import { IContainer, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; -import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; +import { + VisualizeByReferenceInput, + VisualizeByValueInput, + VisualizeEmbeddable, + VisualizeInput, + VisualizeOutput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { SerializedVis, Vis } from '../vis'; import { @@ -43,13 +50,16 @@ import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_obje import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { AttributeService } from '../../../dashboard/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; } export interface VisualizeEmbeddableFactoryDeps { - start: StartServicesGetter>; + start: StartServicesGetter< + Pick + >; } export class VisualizeEmbeddableFactory @@ -62,6 +72,12 @@ export class VisualizeEmbeddableFactory > { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; + private attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >; + public readonly savedObjectMetaData: SavedObjectMetaData = { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), includeFields: ['visState'], @@ -105,6 +121,19 @@ export class VisualizeEmbeddableFactory return await this.deps.start().core.application.currentAppId$.pipe(first()).toPromise(); } + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = await this.deps + .start() + .plugins.dashboard.getAttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >(this.type, { customSaveMethod: this.onSave }); + } + return this.attributeService!; + } + public async createFromSavedObject( savedObjectId: string, input: Partial & { id: string }, @@ -117,7 +146,13 @@ export class VisualizeEmbeddableFactory const visState = convertToSerializedVis(savedObject); const vis = new Vis(savedObject.visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); @@ -131,7 +166,14 @@ export class VisualizeEmbeddableFactory const visState = input.savedVis; const vis = new Vis(visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + const savedVisualizations = getSavedVisualizationsLoader(); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } else { showNewVisModal({ originatingApp: await this.getCurrentAppId(), @@ -140,4 +182,47 @@ export class VisualizeEmbeddableFactory return undefined; } } + + private async onSave( + type: string, + attributes: VisualizeSavedObjectAttributes + ): Promise<{ id: string }> { + try { + const { title, savedVis } = attributes; + const visObj = attributes.vis; + if (!savedVis) { + throw new Error('No Saved Vis'); + } + const saveOptions = { + confirmOverwrite: false, + returnToOrigin: true, + }; + savedVis.title = title; + savedVis.copyOnSave = false; + savedVis.description = ''; + savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); + const serializedVis = ((visObj as unknown) as Vis).serialize(); + const { params, data } = serializedVis; + savedVis.visState = { + title, + type: serializedVis.type, + params, + aggs: data.aggs, + }; + if (visObj) { + savedVis.uiStateJSON = visObj?.uiState.toString(); + } + const id = await savedVis.save(saveOptions); + if (!id || id === '') { + throw new Error( + i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { + defaultMessage: 'Saving a visualization failed', + }) + ); + } + return { id }; + } catch (error) { + throw error; + } + } } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index e0ec4801b3caf..646acc49a6a83 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -27,6 +27,7 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; +import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -69,6 +70,8 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), + dashboard: dashboardPluginMock.createStartContract(), + getAttributeService: jest.fn(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 3546fa4056491..0ba80887b513f 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -76,6 +76,7 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; +import { DashboardStart } from '../../dashboard/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -109,6 +110,8 @@ export interface VisualizationsStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; + dashboard: DashboardStart; + getAttributeService: DashboardStart['getAttributeService']; } /** @@ -155,7 +158,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts similarity index 74% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts rename to x-pack/plugins/apm/common/anomaly_detection.test.ts index 52b7d54236db6..21963b5300f83 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity, severity } from './getSeverity'; +import { getSeverity, Severity } from './anomaly_detection'; describe('getSeverity', () => { describe('when score is undefined', () => { @@ -15,25 +15,25 @@ describe('getSeverity', () => { describe('when score < 25', () => { it('returns warning', () => { - expect(getSeverity(10)).toEqual(severity.warning); + expect(getSeverity(10)).toEqual(Severity.warning); }); }); describe('when score is between 25 and 50', () => { it('returns minor', () => { - expect(getSeverity(40)).toEqual(severity.minor); + expect(getSeverity(40)).toEqual(Severity.minor); }); }); describe('when score is between 50 and 75', () => { it('returns major', () => { - expect(getSeverity(60)).toEqual(severity.major); + expect(getSeverity(60)).toEqual(Severity.major); }); }); describe('when score is 75 or more', () => { it('returns critical', () => { - expect(getSeverity(100)).toEqual(severity.critical); + expect(getSeverity(100)).toEqual(Severity.critical); }); }); }); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 07270b572a4be..5d80ee6381267 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EuiTheme } from '../../../legacy/common/eui_styled_components'; export interface ServiceAnomalyStats { transactionType?: string; @@ -13,6 +14,82 @@ export interface ServiceAnomalyStats { jobId?: string; } +export enum Severity { + critical = 'critical', + major = 'major', + minor = 'minor', + warning = 'warning', +} + +// TODO: Replace with `getSeverity` from: +// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 +export function getSeverity(score?: number) { + if (typeof score !== 'number') { + return undefined; + } else if (score < 25) { + return Severity.warning; + } else if (score >= 25 && score < 50) { + return Severity.minor; + } else if (score >= 50 && score < 75) { + return Severity.major; + } else if (score >= 75) { + return Severity.critical; + } else { + return undefined; + } +} + +export function getSeverityColor(theme: EuiTheme, severity?: Severity) { + switch (severity) { + case Severity.warning: + return theme.eui.euiColorVis0; + case Severity.minor: + case Severity.major: + return theme.eui.euiColorVis5; + case Severity.critical: + return theme.eui.euiColorVis9; + default: + return; + } +} + +export function getSeverityLabel(severity?: Severity) { + switch (severity) { + case Severity.critical: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.critical', + { + defaultMessage: 'Critical', + } + ); + + case Severity.major: + case Severity.minor: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.warning', + { + defaultMessage: 'Warning', + } + ); + + case Severity.warning: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.healthy', + { + defaultMessage: 'Healthy', + } + ); + + default: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.unknown', + { + defaultMessage: 'Unknown', + } + ); + } +} + export const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/common/service_map.test.ts b/x-pack/plugins/apm/common/service_map.test.ts index 346403efc46ae..31f439a7aaec9 100644 --- a/x-pack/plugins/apm/common/service_map.test.ts +++ b/x-pack/plugins/apm/common/service_map.test.ts @@ -8,7 +8,7 @@ import { License } from '../../licensing/common/license'; import * as serviceMap from './service_map'; describe('service map helpers', () => { - describe('isValidPlatinumLicense', () => { + describe('isActivePlatinumLicense', () => { describe('with an expired license', () => { it('returns false', () => { const license = new License({ @@ -22,7 +22,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -39,7 +39,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -56,7 +56,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -73,7 +73,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -90,7 +90,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 7f46fc685d9ca..1dc4d598cd2ee 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -46,7 +46,7 @@ export interface ServiceNodeStats { avgErrorRate: number | null; } -export function isValidPlatinumLicense(license: ILicense) { +export function isActivePlatinumLicense(license: ILicense) { return license.isActive && license.hasAtLeast('platinum'); } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index 66d604a663fbf..ab2bf20b36ed4 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -12,7 +12,7 @@ export const DEFAULT_TIMEOUT = 60 * 1000; Given(`a user browses the APM UI application`, () => { // open service overview page - loginAndWaitForPage(`/app/apm#/services`, { + loginAndWaitForPage(`/app/apm/services`, { from: '2020-06-01T14:59:32.686Z', to: '2020-06-16T16:59:36.219Z', }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 804974d8d437d..31aef30c4e23f 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -15,7 +15,7 @@ Given(`a user browses the APM UI application for RUM Data`, () => { // open service overview page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; - loginAndWaitForPage(`/app/apm#/rum-preview`, { + loginAndWaitForPage(`/app/apm/rum-preview`, { from: RANGE_FROM, to: RANGE_TO, }); diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index a4a6a52e4e4f5..8aa4417580337 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -7,7 +7,8 @@ "apmOss", "data", "licensing", - "triggers_actions_ui" + "triggers_actions_ui", + "embeddable" ], "optionalPlugins": [ "cloud", @@ -22,15 +23,13 @@ ], "server": true, "ui": true, - "configPath": [ - "xpack", - "apm" - ], + "configPath": ["xpack", "apm"], "extraPublicDirs": ["public/style/variables"], "requiredBundles": [ "kibanaReact", "kibanaUtils", "observability", - "home" + "home", + "maps" ] } diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index d76ed5c2100b2..488edd2c92c43 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -11,7 +11,7 @@ import styled, { ThemeProvider, DefaultTheme } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { CoreStart, AppMountParameters } from 'kibana/public'; -import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { KibanaContextProvider, @@ -72,11 +72,13 @@ export function CsmAppRoot({ deps, history, config, + corePlugins: { embeddable }, }: { core: CoreStart; deps: ApmPluginSetupDeps; history: AppMountParameters['history']; config: ConfigSchema; + corePlugins: ApmPluginStartDeps; }) { const i18nCore = core.i18n; const plugins = deps; @@ -88,7 +90,7 @@ export function CsmAppRoot({ return ( - + @@ -112,12 +114,19 @@ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, { element, history }: AppMountParameters, - config: ConfigSchema + config: ConfigSchema, + corePlugins: ApmPluginStartDeps ) => { createCallApmApi(core.http); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx index 213126ba4bf81..34fcf62178711 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx @@ -31,9 +31,10 @@ interface Props { count: number; name: string; }>; + loading: boolean; } -export function VisitorBreakdownChart({ options }: Props) { +export function VisitorBreakdownChart({ loading, options }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); const euiChartTheme = darkMode @@ -41,7 +42,7 @@ export function VisitorBreakdownChart({ options }: Props) { : EUI_CHARTS_THEME_LIGHT; return ( - + - ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx index e8305a6aef0d4..d0bd674ef5c79 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx @@ -20,10 +20,11 @@ const CoreVitalsThresholds = { export function CoreVitals() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; + const { start, end } = urlParams; const { data, status } = useFetcher( (callApmApi) => { + const { serviceName } = uiFilters; if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', @@ -34,7 +35,7 @@ export function CoreVitals() { } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters] + [start, end, uiFilters] ); const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index f05c07e8512ac..48c0f6cc60d84 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -18,6 +18,7 @@ import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; import { CoreVitals } from './CoreVitals'; +import { VisitorBreakdownMap } from './VisitorBreakdownMap'; export function RumDashboard() { return ( @@ -67,6 +68,9 @@ export function RumDashboard() { + + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index e18875f32ff72..245f58370d3d7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -16,7 +16,7 @@ export function VisitorBreakdown() { const { start, end } = urlParams; - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -47,14 +47,20 @@ export function VisitorBreakdown() {

{I18LABELS.browser}

- +

{I18LABELS.operatingSystem}

- +
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx new file mode 100644 index 0000000000000..93608a0ccd826 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useRef } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; + +import { + MapEmbeddable, + MapEmbeddableInput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../maps/public/embeddable'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { getLayerList } from './LayerList'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { MapToolTip } from './MapToolTip'; +import { useMapFilters } from './useMapFilters'; +import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +interface KibanaDeps { + embeddable: EmbeddableStart; +} +export function EmbeddedMapComponent() { + const { urlParams } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const mapFilters = useMapFilters(); + + const [embeddable, setEmbeddable] = useState< + MapEmbeddable | ErrorEmbeddable | undefined + >(); + + const embeddableRoot: React.RefObject = useRef< + HTMLDivElement + >(null); + + const { + services: { embeddable: embeddablePlugin }, + } = useKibana(); + + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory( + MAP_SAVED_OBJECT_TYPE + ); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + filters: mapFilters, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + query: { + query: 'transaction.type : "page-load"', + language: 'kuery', + }, + ...(start && { + timeRange: { + from: new Date(start!).toISOString(), + to: new Date(end!).toISOString(), + }, + }), + hideFilterActions: true, + }; + + function renderTooltipContent({ + addFilters, + closeTooltip, + features, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }: RenderTooltipContentParams) { + const props = { + addFilters, + closeTooltip, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }; + + return ; + } + + useEffect(() => { + if (embeddable != null && serviceName) { + embeddable.updateInput({ filters: mapFilters }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mapFilters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && start != null && end != null) { + const timeRange = { + from: new Date(start).toISOString(), + to: new Date(end).toISOString(), + }; + embeddable.updateInput({ timeRange }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [start, end]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: 'Visitors by region', + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setRenderTooltipContent(renderTooltipContent); + await embeddableObject.setLayerList(getLayerList()); + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + return ( + +
+ + ); +} + +EmbeddedMapComponent.displayName = 'EmbeddedMap'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts new file mode 100644 index 0000000000000..138a3f4018c65 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EMSFileSourceDescriptor, + EMSTMSSourceDescriptor, + ESTermSourceDescriptor, + LayerDescriptor as BaseLayerDescriptor, + VectorLayerDescriptor as BaseVectorLayerDescriptor, + VectorStyleDescriptor, +} from '../../../../../../maps/common/descriptor_types'; +import { + AGG_TYPE, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + LABEL_BORDER_SIZES, + STYLE_TYPE, + SYMBOLIZE_AS_TYPES, +} from '../../../../../../maps/common/constants'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const ES_TERM_SOURCE: ESTermSourceDescriptor = { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: AGG_TYPE.AVG, + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + applyGlobalQuery: true, +}; + +export const REGION_NAME = 'region_name'; +export const COUNTRY_NAME = 'name'; + +export const TRANSACTION_DURATION_REGION = + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41'; + +export const TRANSACTION_DURATION_COUNTRY = + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c'; + +interface LayerDescriptor extends BaseLayerDescriptor { + sourceDescriptor: EMSTMSSourceDescriptor; +} + +interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { + sourceDescriptor: EMSFileSourceDescriptor; +} + +export function getLayerList() { + const baseLayer: LayerDescriptor = { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }; + + const getLayerStyle = (fieldName: string): VectorStyleDescriptor => { + return { + type: 'VECTOR', + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: fieldName, + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } }, + iconOrientation: { + type: STYLE_TYPE.STATIC, + options: { orientation: 0 }, + }, + labelText: { type: STYLE_TYPE.STATIC, options: { value: '' } }, + labelColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#000000' }, + }, + labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } }, + labelBorderColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#FFFFFF' }, + }, + symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } }, + labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } }, + }, + isTimeAware: true, + }; + }; + + const pageLoadDurationByCountryLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'iso2', + right: ES_TERM_SOURCE, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: [COUNTRY_NAME], + applyGlobalQuery: true, + }, + style: getLayerStyle(TRANSACTION_DURATION_COUNTRY), + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + + const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', REGION_NAME], + }, + style: getLayerStyle(TRANSACTION_DURATION_REGION), + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + return [ + baseLayer, + pageLoadDurationByCountryLayer, + pageLoadDurationByAdminRegionLayer, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx new file mode 100644 index 0000000000000..07b40addedec3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiOutsideClickDetector, + EuiPopoverTitle, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { + COUNTRY_NAME, + REGION_NAME, + TRANSACTION_DURATION_COUNTRY, + TRANSACTION_DURATION_REGION, +} from './LayerList'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { I18LABELS } from '../translations'; + +type MapToolTipProps = Partial; + +const DescriptionItem = styled(EuiDescriptionListDescription)` + &&& { + width: 25%; + } +`; + +const TitleItem = styled(EuiDescriptionListTitle)` + &&& { + width: 75%; + } +`; + +function MapToolTipComponent({ + closeTooltip, + features = [], + loadFeatureProperties, +}: MapToolTipProps) { + const { id: featureId, layerId } = features[0] ?? {}; + + const [regionName, setRegionName] = useState(featureId as string); + const [pageLoadDuration, setPageLoadDuration] = useState(''); + + const formatPageLoadValue = (val: number) => { + const valInMs = val / 1000; + if (valInMs > 1000) { + return (valInMs / 1000).toFixed(2) + ' sec'; + } + + return (valInMs / 1000).toFixed(0) + ' ms'; + }; + + useEffect(() => { + const loadRegionInfo = async () => { + if (loadFeatureProperties) { + const items = await loadFeatureProperties({ layerId, featureId }); + items.forEach((item) => { + if ( + item.getPropertyKey() === COUNTRY_NAME || + item.getPropertyKey() === REGION_NAME + ) { + setRegionName(item.getRawValue() as string); + } + if ( + item.getPropertyKey() === TRANSACTION_DURATION_REGION || + item.getPropertyKey() === TRANSACTION_DURATION_COUNTRY + ) { + setPageLoadDuration( + formatPageLoadValue(+(item.getRawValue() as string)) + ); + } + }); + } + }; + loadRegionInfo(); + }); + + return ( + { + if (closeTooltip != null) { + closeTooltip(); + } + }} + > + <> + {regionName} + + + {I18LABELS.avgPageLoadDuration} + + {pageLoadDuration} + + + + ); +} + +export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx new file mode 100644 index 0000000000000..023f5d61a964e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { MapToolTip } from '../MapToolTip'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList'; + +storiesOf('app/RumDashboard/VisitorsRegionMap', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Tooltip', + () => { + const loadFeatureProps = async () => { + return [ + { + getPropertyKey: () => COUNTRY_NAME, + getRawValue: () => 'United States', + }, + { + getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, + getRawValue: () => 2434353, + }, + ]; + }; + return ( + + ); + }, + { + info: { + propTables: false, + source: false, + }, + } + ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx new file mode 100644 index 0000000000000..790be81bb65c0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from 'enzyme'; +import React from 'react'; + +import { EmbeddedMap } from '../EmbeddedMap'; +import { KibanaContextProvider } from '../../../../../../../security_solution/public/common/lib/kibana'; +import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks'; + +describe('Embedded Map', () => { + test('it renders', () => { + const [core] = mockCore(); + + const wrapper = render( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +const mockEmbeddable = embeddablePluginMock.createStartContract(); + +mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ + create: () => ({ + reload: jest.fn(), + setRenderTooltipContent: jest.fn(), + setLayerList: jest.fn(), + }), +})); + +const mockCore: () => [any] = () => { + const core = { + embeddable: mockEmbeddable, + }; + + return [core]; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts new file mode 100644 index 0000000000000..eb149ee2a132d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockLayerList } from './__mocks__/regions_layer.mock'; +import { getLayerList } from '../LayerList'; + +describe('LayerList', () => { + describe('getLayerList', () => { + test('it returns the region layer', () => { + const layerList = getLayerList(); + expect(layerList).toStrictEqual(mockLayerList); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx new file mode 100644 index 0000000000000..cbaae40b04361 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render, shallow } from 'enzyme'; +import React from 'react'; + +import { MapToolTip } from '../MapToolTip'; + +describe('Map Tooltip', () => { + test('it shallow renders', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders', () => { + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts new file mode 100644 index 0000000000000..c45f8b27d7d3e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockLayerList = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }, + { + joins: [ + { + leftField: 'iso2', + right: { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: 'avg', + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: 'apm_static_index_pattern_id', + applyGlobalQuery: true, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: ['name'], + applyGlobalQuery: true, + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, + { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: 'avg', field: 'transaction.duration.us' }], + indexPatternId: 'apm_static_index_pattern_id', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', 'region_name'], + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, +]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap new file mode 100644 index 0000000000000..67f79c9fc747e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Embedded Map it renders 1`] = ` +.c0 { + z-index: auto; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + position: relative; +} + +.c0 .embPanel__content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 1 100%; + -ms-flex: 1 1 100%; + flex: 1 1 100%; + z-index: 1; + min-height: 0; +} + +.c0.c0.c0 .mapboxgl-canvas { + -webkit-animation: none !important; + animation: none !important; +} + +
+
+
+`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap new file mode 100644 index 0000000000000..860727a7a0f86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Map Tooltip it renders 1`] = ` +Array [ +
, + .c1.c1.c1 { + width: 25%; +} + +.c0.c0.c0 { + width: 75%; +} + +
+
+ Average page load duration +
+
+
, +] +`; + +exports[`Map Tooltip it shallow renders 1`] = ` + + + + + Average page load duration + + + + +`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx new file mode 100644 index 0000000000000..44bfe5abbaca2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EmbeddedMap } from './EmbeddedMap'; +import { I18LABELS } from '../translations'; + +export function VisitorBreakdownMap() { + return ( + <> + +

{I18LABELS.pageLoadDurationByRegion}

+
+ +
+ +
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts new file mode 100644 index 0000000000000..357e04c538e68 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; +import { + CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, +} from '../../../../../common/elasticsearch_fieldnames'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const getMatchFilter = (field: string, value: string): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: field, + params: { query: value }, + }, + query: { match_phrase: { [field]: value } }, + }; +}; + +const getMultiMatchFilter = (field: string, values: string[]): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + type: 'phrases', + key: field, + value: values.join(', '), + params: values, + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: values.map((value) => ({ match_phrase: { [field]: value } })), + minimum_should_match: 1, + }, + }, + }; +}; +export const useMapFilters = (): Filter[] => { + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName } = urlParams; + + const { browser, device, os, location } = uiFilters; + + const [mapFilters, setMapFilters] = useState([]); + + const existFilter: Filter = { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'transaction.marks.navigationTiming.fetchStart', + value: 'exists', + }, + exists: { + field: 'transaction.marks.navigationTiming.fetchStart', + }, + }; + + useEffect(() => { + const filters = [existFilter]; + if (serviceName) { + filters.push(getMatchFilter(SERVICE_NAME, serviceName)); + } + if (browser) { + filters.push(getMultiMatchFilter(USER_AGENT_NAME, browser)); + } + if (device) { + filters.push(getMultiMatchFilter(USER_AGENT_DEVICE, device)); + } + if (os) { + filters.push(getMultiMatchFilter(USER_AGENT_OS, os)); + } + if (location) { + filters.push(getMultiMatchFilter(CLIENT_GEO_COUNTRY_ISO_CODE, location)); + } + + setMapFilters(filters); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serviceName, browser, device, os, location]); + + return mapFilters; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8d1959ec14d15..fa0551252b6a1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -58,7 +58,7 @@ export function RumOverview() { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 660ed5a92a0e6..ec135168729b4 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -64,6 +64,18 @@ export const I18LABELS = { defaultMessage: 'Operating system', } ), + avgPageLoadDuration: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration', + { + defaultMessage: 'Average page load duration', + } + ), + pageLoadDurationByRegion: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion', + { + defaultMessage: 'Page load duration by region', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index b3d19e1aab2cc..5699d0b56219b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -18,10 +18,13 @@ import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor, popoverWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscapeOptions'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; -import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; -import { getSeverity } from './getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, +} from '../../../../../common/anomaly_detection'; const HealthStatusTitle = styled(EuiTitle)` display: inline; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx index 55d068cba5935..8670cf623c253 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -25,11 +25,7 @@ export function Buttons({ }: ButtonsProps) { const { core } = useApmPluginContext(); const { basePath } = core.http; - // The params may contain the service name. We want to use the selected node's - // service name in the button URLs, so make a copy and set the - // `serviceName` property. - const urlParams = { ...useUrlParams().urlParams } as APMQueryParams; - urlParams.serviceName = selectedNodeServiceName; + const urlParams = useUrlParams().urlParams as APMQueryParams; const detailsUrl = getAPMHref({ basePath, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts deleted file mode 100644 index f4eb2033e9231..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum severity { - critical = 'critical', - major = 'major', - minor = 'minor', - warning = 'warning', -} - -// TODO: Replace with `getSeverity` from: -// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 -export function getSeverity(score?: number) { - if (typeof score !== 'number') { - return undefined; - } else if (score < 25) { - return severity.warning; - } else if (score >= 25 && score < 50) { - return severity.minor; - } else if (score >= 50 && score < 75) { - return severity.major; - } else if (score >= 75) { - return severity.critical; - } else { - return undefined; - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 9fedcc70bbbcf..1ac7157cc2aad 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -11,25 +11,15 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; import { defaultIcon, iconForNode } from './icons'; -import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; -import { severity, getSeverity } from './Popover/getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, + Severity, +} from '../../../../common/anomaly_detection'; export const popoverWidth = 280; -export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { - switch (nodeSeverity) { - case severity.warning: - return theme.eui.euiColorVis0; - case severity.minor: - case severity.major: - return theme.eui.euiColorVis5; - case severity.critical: - return theme.eui.euiColorVis9; - default: - return; - } -} - function getNodeSeverity(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( 'serviceAnomalyStats' @@ -60,7 +50,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.critical) { + if (nodeSeverity === Severity.critical) { return 'double'; } else { return 'solid'; @@ -70,9 +60,9 @@ const getBorderStyle: cytoscape.Css.MapperFunction< function getBorderWidth(el: cytoscape.NodeSingular) { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { + if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) { return 4; - } else if (nodeSeverity === severity.critical) { + } else if (nodeSeverity === Severity.critical) { return 8; } else { return 4; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index 2f4cc0d39d71c..c85cf85d38702 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -5,7 +5,6 @@ */ import cytoscape from 'cytoscape'; -import { getNormalizedAgentName } from '../../../../common/agent_name'; import { AGENT_NAME, SPAN_SUBTYPE, @@ -13,29 +12,22 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import awsIcon from './icons/aws.svg'; import cassandraIcon from './icons/cassandra.svg'; -import darkIcon from './icons/dark.svg'; import databaseIcon from './icons/database.svg'; import defaultIconImport from './icons/default.svg'; import documentsIcon from './icons/documents.svg'; -import dotNetIcon from './icons/dot-net.svg'; import elasticsearchIcon from './icons/elasticsearch.svg'; import globeIcon from './icons/globe.svg'; -import goIcon from './icons/go.svg'; import graphqlIcon from './icons/graphql.svg'; import grpcIcon from './icons/grpc.svg'; import handlebarsIcon from './icons/handlebars.svg'; -import javaIcon from './icons/java.svg'; import kafkaIcon from './icons/kafka.svg'; import mongodbIcon from './icons/mongodb.svg'; import mysqlIcon from './icons/mysql.svg'; -import nodeJsIcon from './icons/nodejs.svg'; -import phpIcon from './icons/php.svg'; import postgresqlIcon from './icons/postgresql.svg'; -import pythonIcon from './icons/python.svg'; import redisIcon from './icons/redis.svg'; -import rubyIcon from './icons/ruby.svg'; -import rumJsIcon from './icons/rumjs.svg'; import websocketIcon from './icons/websocket.svg'; +import javaIcon from '../../shared/AgentIcon/icons/java.svg'; +import { getAgentIcon } from '../../shared/AgentIcon/get_agent_icon'; export const defaultIcon = defaultIconImport; @@ -74,23 +66,6 @@ const typeIcons: { [key: string]: { [key: string]: string } } = { }, }; -const agentIcons: { [key: string]: string } = { - dark: darkIcon, - dotnet: dotNetIcon, - go: goIcon, - java: javaIcon, - 'js-base': rumJsIcon, - nodejs: nodeJsIcon, - php: phpIcon, - python: pythonIcon, - ruby: rubyIcon, -}; - -function getAgentIcon(agentName?: string) { - const normalizedAgentName = getNormalizedAgentName(agentName); - return normalizedAgentName && agentIcons[normalizedAgentName]; -} - function getSpanIcon(type?: string, subtype?: string) { if (!type) { return; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg deleted file mode 100644 index 9ae4b31c1a0d6..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 83fab95bc91c9..cb5a57e9ab9fb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useTheme } from '../../../hooks/useTheme'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; @@ -36,7 +36,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const { data = { elements: [] } } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. - if (!license || !isValidPlatinumLicense(license)) { + if (!license || !isActivePlatinumLicense(license)) { return; } @@ -66,7 +66,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isValidPlatinumLicense(license) ? ( + return isActivePlatinumLicense(license) ? (
+ {getSeverityLabel(severity)} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx new file mode 100644 index 0000000000000..dd632db0f15fe --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiButton } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { APMLink } from '../../../shared/Links/apm/APMLink'; + +export function MLCallout({ onDismiss }: { onDismiss: () => void }) { + return ( + +

+ {i18n.translate('xpack.apm.serviceOverview.mlNudgeMessage.content', { + defaultMessage: `Our integration with ML anomaly detection will enable you to see your services' health status`, + })} +

+ + + + + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton', + { + defaultMessage: `Learn more`, + } + )} + + + + + onDismiss()}> + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', + { + defaultMessage: `Dismiss message`, + } + )} + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx new file mode 100644 index 0000000000000..c94c94d4a0b72 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../hooks/useTheme'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; +import { SparkPlot } from '../../../shared/charts/SparkPlot'; + +export function ServiceListMetric({ + color, + series, + valueLabel, +}: { + color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const { + urlParams: { start, end }, + } = useUrlParams(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 927779b571fd8..519d74827097b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -15,34 +15,62 @@ describe('ServiceOverview -> List', () => { mockMoment(); }); - it('should render empty state', () => { + it('renders empty state', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render with data', () => { + it('renders with data', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render columns correctly', () => { + it('renders columns correctly', () => { const service = { serviceName: 'opbeans-python', agentName: 'python', - transactionsPerMinute: 86.93333333333334, - errorsPerMinute: 12.6, - avgResponseTime: 91535.42944785276, + transactionsPerMinute: { + value: 86.93333333333334, + timeseries: [], + }, + errorsPerMinute: { + value: 12.6, + timeseries: [], + }, + avgResponseTime: { + value: 91535.42944785276, + timeseries: [], + }, environments: ['test'], }; const renderedColumns = SERVICE_COLUMNS.map((c) => c.render(service[c.field], service) ); + expect(renderedColumns[0]).toMatchSnapshot(); - expect(renderedColumns.slice(2)).toEqual([ - 'python', - '92 ms', - '86.9 tpm', - '12.6 err.', - ]); + }); + + describe('without ML data', () => { + it('does not render health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).not.toBe('severity'); + }); + }); + + describe('with ML data', () => { + it('renders health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).toBe('severity'); + }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 146f6f58031bb..da3f6ae89940a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,21 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ServiceOverview -> List should render columns correctly 1`] = ` - - - opbeans-python - - -`; +exports[`ServiceOverview -> List renders columns correctly 1`] = ``; -exports[`ServiceOverview -> List should render empty state 1`] = ` +exports[`ServiceOverview -> List renders empty state 1`] = ` List should render empty state 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={Array []} + sortFn={[Function]} /> `; -exports[`ServiceOverview -> List should render with data 1`] = ` +exports[`ServiceOverview -> List renders with data 1`] = ` List should render with data 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={ Array [ Object { @@ -125,19 +115,35 @@ exports[`ServiceOverview -> List should render with data 1`] = ` "environments": Array [ "test", ], - "errorsPerMinute": 46.06666666666667, + "errorsPerMinute": Object { + "timeseries": Array [], + "value": 46.06666666666667, + }, "serviceName": "opbeans-node", - "transactionsPerMinute": 0, + "transactionsPerMinute": Object { + "timeseries": Array [], + "value": 0, + }, }, Object { "agentName": "python", - "avgResponseTime": 91535.42944785276, + "avgResponseTime": Object { + "timeseries": Array [], + "value": 91535.42944785276, + }, "environments": Array [], - "errorsPerMinute": 12.6, + "errorsPerMinute": Object { + "timeseries": Array [], + "value": 12.6, + }, "serviceName": "opbeans-python", - "transactionsPerMinute": 86.93333333333334, + "transactionsPerMinute": Object { + "timeseries": Array [], + "value": 86.93333333333334, + }, }, ] } + sortFn={[Function]} /> `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json index 2379d27407e04..7f24ad8b0d308 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json @@ -3,17 +3,34 @@ { "serviceName": "opbeans-node", "agentName": "nodejs", - "transactionsPerMinute": 0, - "errorsPerMinute": 46.06666666666667, + "transactionsPerMinute": { + "value": 0, + "timeseries": [] + }, + "errorsPerMinute": { + "value": 46.06666666666667, + "timeseries": [] + }, "avgResponseTime": null, - "environments": ["test"] + "environments": [ + "test" + ] }, { "serviceName": "opbeans-python", "agentName": "python", - "transactionsPerMinute": 86.93333333333334, - "errorsPerMinute": 12.6, - "avgResponseTime": 91535.42944785276, + "transactionsPerMinute": { + "value": 86.93333333333334, + "timeseries": [] + }, + "errorsPerMinute": { + "value": 12.6, + "timeseries": [] + }, + "avgResponseTime": { + "value": 91535.42944785276, + "timeseries": [] + }, "environments": [] } ] diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 90cc9af45273e..ce256137481cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,24 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiToolTip } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; +import { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { asPercent } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { fontSizes, truncate } from '../../../../style/variables'; +import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters'; -import { ManagedTable } from '../../../shared/ManagedTable'; +import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; +import { AgentIcon } from '../../../shared/AgentIcon'; +import { Severity } from '../../../../../common/anomaly_detection'; +import { HealthBadge } from './HealthBadge'; +import { ServiceListMetric } from './ServiceListMetric'; interface Props { items: ServiceListAPIResponse['items']; noItemsMessage?: React.ReactNode; + displayHealthStatus: boolean; } +type ServiceListItem = ValuesType; + function formatNumber(value: number) { if (value === 0) { return '0'; @@ -41,7 +51,18 @@ const AppLink = styled(TransactionOverviewLink)` ${truncate('100%')}; `; -export const SERVICE_COLUMNS = [ +export const SERVICE_COLUMNS: Array> = [ + { + field: 'severity', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: px(unit * 6), + sortable: true, + render: (_, { severity }) => { + return ; + }, + }, { field: 'serviceName', name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { @@ -49,9 +70,24 @@ export const SERVICE_COLUMNS = [ }), width: '40%', sortable: true, - render: (serviceName: string) => ( - - {formatString(serviceName)} + render: (_, { serviceName, agentName }) => ( + + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + + + ), }, @@ -60,20 +96,12 @@ export const SERVICE_COLUMNS = [ name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { defaultMessage: 'Environment', }), - width: '20%', + width: px(unit * 10), sortable: true, - render: (environments: string[]) => ( - + render: (_, { environments }) => ( + ), }, - { - field: 'agentName', - name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', { - defaultMessage: 'Agent', - }), - sortable: true, - render: (agentName: string) => formatString(agentName), - }, { field: 'avgResponseTime', name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { @@ -81,7 +109,15 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (time: number) => asMillisecondDuration(time), + render: (_, { avgResponseTime }) => ( + + ), + align: 'left', + width: px(unit * 10), }, { field: 'transactionsPerMinute', @@ -93,39 +129,107 @@ export const SERVICE_COLUMNS = [ ), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel', - { - defaultMessage: 'tpm', - } - )}`, + render: (_, { transactionsPerMinute }) => ( + + ), + align: 'left', + width: px(unit * 10), }, { field: 'errorsPerMinute', - name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', { - defaultMessage: 'Errors per minute', + name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { + defaultMessage: 'Error rate %', }), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.errorsPerMinuteUnitLabel', - { - defaultMessage: 'err.', - } - )}`, + render: (_, { transactionErrorRate }) => { + const value = transactionErrorRate?.value; + + const valueLabel = + value !== null && value !== undefined ? asPercent(value, 1) : ''; + + return ( + + ); + }, + align: 'left', + width: px(unit * 10), }, ]; -export function ServiceList({ items, noItemsMessage }: Props) { +const SEVERITY_ORDER = [ + Severity.warning, + Severity.minor, + Severity.major, + Severity.critical, +]; + +export function ServiceList({ + items, + displayHealthStatus, + noItemsMessage, +}: Props) { + const columns = displayHealthStatus + ? SERVICE_COLUMNS + : SERVICE_COLUMNS.filter((column) => column.field !== 'severity'); + return ( { + // For severity, sort items by severity first, then by TPM + + return sortField === 'severity' + ? orderBy( + itemsToSort, + [ + (item) => { + return item.severity + ? SEVERITY_ORDER.indexOf(item.severity) + : -1; + }, + (item) => item.transactionsPerMinute?.value ?? 0, + ], + [sortDirection, sortDirection] + ) + : orderBy( + itemsToSort, + (item) => { + switch (sortField) { + case 'avgResponseTime': + return item.avgResponseTime?.value ?? 0; + case 'transactionsPerMinute': + return item.transactionsPerMinute?.value ?? 0; + case 'transactionErrorRate': + return item.transactionErrorRate?.value ?? 0; + + default: + return item[sortField as keyof typeof item]; + } + }, + sortDirection + ); + }} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index d9c5ff5130df6..8eeff018ad03f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -8,6 +8,7 @@ import { render, wait, waitForElement } from '@testing-library/react'; import { CoreStart } from 'kibana/public'; import React, { FunctionComponent, ReactChild } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { merge } from 'lodash'; import { ServiceOverview } from '..'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; import { @@ -17,35 +18,38 @@ import { import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; +import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; +import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const addWarning = jest.fn(); +const httpGet = jest.fn(); + function wrapper({ children }: { children: ReactChild }) { + const mockPluginContext = (merge({}, mockApmPluginContextValue, { + core: { + http: { + get: httpGet, + }, + notifications: { + toasts: { + addWarning, + }, + }, + }, + }) as unknown) as ApmPluginContextValue; + return ( - - {children} - + + + {children} + + ); } @@ -56,9 +60,6 @@ function renderServiceOverview() { }); } -const addWarning = jest.fn(); -const httpGet = jest.fn(); - describe('Service Overview -> View', () => { beforeEach(() => { // @ts-expect-error @@ -80,6 +81,17 @@ describe('Service Overview -> View', () => { clearValues: () => null, status: FETCH_STATUS.SUCCESS, }); + + jest + .spyOn(useAnomalyDetectionJobs, 'useAnomalyDetectionJobs') + .mockReturnValue({ + status: FETCH_STATUS.SUCCESS, + data: { + jobs: [], + hasLegacyJobs: false, + }, + refetch: () => undefined, + }); }); afterEach(() => { @@ -99,6 +111,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], + severity: 1, }, { serviceName: 'My Go Service', @@ -107,6 +120,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 500, avgResponseTime: 600, environments: [], + severity: 10, }, ], }); @@ -195,4 +209,57 @@ describe('Service Overview -> View', () => { expect(addWarning).not.toHaveBeenCalled(); }); }); + + describe('when ML data is not found', () => { + it('does not render the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + }, + ], + }); + + const { queryByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryByText('Health')).toBeNull(); + }); + }); + + describe('when ML data is found', () => { + it('renders the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + severity: 1, + }, + ], + }); + + const { queryAllByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryAllByText('Health').length).toBeGreaterThan(1); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 6d447887627bf..b56f7d6820274 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -7,7 +7,7 @@ NodeList [ >
- Name + Health
- - My Go Service - + + Unknown + +
- Environment + Name
+ > + + + +
- Agent + Environment
- go + + + + test + + + + + + + dev + + +
- 0.6 ms +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 ms +
+
- 400.0 tpm +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 tpm +
+
- Errors per minute + Error rate %
- 500.0 err. +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+
, @@ -247,87 +423,91 @@ NodeList [ >
- Name + Health
- - My Python Service - + + Unknown + +
- Environment + Name
- - - test - - - - - - +
+
- dev - - + + My Go Service + +
+
- Agent + Environment
- python -
+ />
- 0.3 ms +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 ms +
+
- 100.0 tpm +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 tpm +
+
- Errors per minute + Error rate %
- 200.0 err. +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+
, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7146e471a7f82..d9d2cffb67620 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher'; import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -18,8 +18,11 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { MLCallout } from './ServiceList/MLCallout'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs'; -const initalData = { +const initialData = { items: [], hasHistoricalData: true, hasLegacyData: false, @@ -33,7 +36,7 @@ export function ServiceOverview() { urlParams: { start, end }, uiFilters, } = useUrlParams(); - const { data = initalData, status } = useFetcher( + const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -93,6 +96,26 @@ export function ServiceOverview() { [] ); + const { + data: anomalyDetectionJobsData, + status: anomalyDetectionJobsStatus, + } = useAnomalyDetectionJobs(); + + const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( + 'apm.userHasDismissedServiceInventoryMlCallout', + false + ); + + const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; + + const displayMlCallout = + anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS && + !anomalyDetectionJobsData?.jobs.length && + canCreateJob && + !userHasDismissedCallout; + + const displayHealthStatus = data.items.some((item) => 'severity' in item); + return ( <> @@ -101,17 +124,27 @@ export function ServiceOverview() { - - + {displayMlCallout ? ( + + setUserHasDismissedCallout(true)} /> + + ) : null} + + + + } /> - } - /> - + + + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts new file mode 100644 index 0000000000000..2475eecee8e34 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getNormalizedAgentName } from '../../../../common/agent_name'; +import dotNetIcon from './icons/dot-net.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; + +const agentIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon, +}; + +export function getAgentIcon(agentName?: string) { + const normalizedAgentName = getNormalizedAgentName(agentName); + return normalizedAgentName && agentIcons[normalizedAgentName]; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx new file mode 100644 index 0000000000000..5646fc05bd28f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { getAgentIcon } from './get_agent_icon'; +import { px } from '../../../style/variables'; + +interface Props { + agentName: AgentName; +} + +export function AgentIcon(props: Props) { + const { agentName } = props; + + const icon = getAgentIcon(agentName); + + return {agentName}; +} diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 549ee228b7afe..b4d716f89169e 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -7,10 +7,9 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import { isEmpty, isEqual, pickBy } from 'lodash'; import React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { fromQuery, toQuery } from '../Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 9fe52aab83641..9db563a0f6ba8 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -33,9 +33,22 @@ interface Props { hidePerPageOptions?: boolean; noItemsMessage?: React.ReactNode; sortItems?: boolean; + sortFn?: ( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' + ) => T[]; pagination?: boolean; } +function defaultSortFn( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' +) { + return orderBy(items, sortField, sortDirection); +} + function UnoptimizedManagedTable(props: Props) { const history = useHistory(); const { @@ -48,6 +61,7 @@ function UnoptimizedManagedTable(props: Props) { hidePerPageOptions = true, noItemsMessage, sortItems = true, + sortFn = defaultSortFn, pagination = true, } = props; @@ -62,11 +76,11 @@ function UnoptimizedManagedTable(props: Props) { const renderedItems = useMemo(() => { const sortedItems = sortItems - ? orderBy(items, sortField, sortDirection as 'asc' | 'desc') + ? sortFn(items, sortField, sortDirection as 'asc' | 'desc') : items; return sortedItems.slice(page * pageSize, (page + 1) * pageSize); - }, [page, pageSize, sortField, sortDirection, items, sortItems]); + }, [page, pageSize, sortField, sortDirection, items, sortItems, sortFn]); const sort = useMemo(() => { return { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx index fcbdb900368ea..5bddfc67200b1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; -import { getSeverityColor } from '../../app/ServiceMap/cytoscapeOptions'; +import { + getSeverityColor, + Severity, +} from '../../../../common/anomaly_detection'; import { useTheme } from '../../../hooks/useTheme'; -import { severity as Severity } from '../../app/ServiceMap/Popover/getSeverity'; type SeverityScore = 0 | 25 | 50 | 75; const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75]; diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 1a91e398cec76..85d975870d9bc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { EuiPanel } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; +import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; @@ -22,16 +21,11 @@ const tickFormatY = (y?: number) => { }; export function ErroneousTransactionsRateChart() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const syncedChartsProps = useChartsSync(); - const { - serviceName, - start, - end, - transactionType, - transactionName, - } = urlParams; + const { start, end, transactionType, transactionName } = urlParams; const { data } = useFetcher(() => { if (serviceName && start && end) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx new file mode 100644 index 0000000000000..18b914afea995 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ScaleType, Chart, Settings, AreaSeries } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { px } from '../../../../style/variables'; +import { useChartTheme } from '../../../../../../observability/public'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; + +interface Props { + color: string; + series: Array<{ x: number; y: number | null }>; +} + +export function SparkPlot(props: Props) { + const { series, color } = props; + const chartTheme = useChartTheme(); + + const isEmpty = series.every((point) => point.y === null); + + if (isEmpty) { + return ( + + + + + + + {NOT_AVAILABLE_LABEL} + + + + ); + } + + return ( + + + + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts new file mode 100644 index 0000000000000..56c58bc82967b --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useFetcher } from './useFetcher'; + +export function useAnomalyDetectionJobs() { + return useFetcher( + (callApmApi) => + callApmApi({ + pathname: `/api/apm/settings/anomaly-detection`, + }), + [], + { showToastOnError: false } + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx index b34cc95a26399..bb947e307437e 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx @@ -8,6 +8,12 @@ import { renderHook } from '@testing-library/react-hooks'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import * as useFetcherModule from './useFetcher'; import { useAvgDurationByBrowser } from './useAvgDurationByBrowser'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} describe('useAvgDurationByBrowser', () => { it('returns data', () => { @@ -19,7 +25,9 @@ describe('useAvgDurationByBrowser', () => { refetch: () => {}, status: 'success' as useFetcherModule.FETCH_STATUS, }); - const { result } = renderHook(() => useAvgDurationByBrowser()); + const { result } = renderHook(() => useAvgDurationByBrowser(), { + wrapper: Wrapper, + }); expect(result.current.data).toEqual([ { diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts index a8312dd0448e6..78dc4210711ef 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts @@ -5,12 +5,13 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useParams } from 'react-router-dom'; +import { getVizColorForIndex } from '../../common/viz_colors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; import { TimeSeries } from '../../typings/timeseries'; -import { getVizColorForIndex } from '../../common/viz_colors'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { if (!data) { @@ -27,8 +28,9 @@ function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { } export function useAvgDurationByBrowser() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName }, + urlParams: { start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts index 818657226cb14..983f949b72961 100644 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts @@ -3,13 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useAvgDurationByCountry() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName }, + urlParams: { start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts new file mode 100644 index 0000000000000..cf37b45045f4d --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; + +export function useLocalStorage(key: string, defaultValue: T) { + const [item, setItem] = useState(getFromStorage()); + + function getFromStorage() { + const storedItem = window.localStorage.getItem(key); + + let toStore: T = defaultValue; + + if (storedItem !== null) { + try { + toStore = JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(key); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${key}`); + } + } + + return toStore; + } + + const updateFromStorage = () => { + const storedItem = getFromStorage(); + setItem(storedItem); + }; + + const saveToStorage = (value: T) => { + if (value === undefined) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, JSON.stringify(value)); + updateFromStorage(); + } + }; + + useEffect(() => { + window.addEventListener('storage', (event: StorageEvent) => { + if (event.key === key) { + updateFromStorage(); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [item, saveToStorage] as const; +} diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 9db78fde2d8c8..08d2300c3254a 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useTransactionBreakdown() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, start, end, transactionName, transactionType }, + urlParams: { start, end, transactionName, transactionType }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts index 2ecd900386496..e66d70a53afa6 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts @@ -5,13 +5,15 @@ */ import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { getTransactionCharts } from '../selectors/chartSelectors'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; export function useTransactionCharts() { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { serviceName, transactionType, start, end, transactionName }, + urlParams: { transactionType, start, end, transactionName }, uiFilters, } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 539918c432783..d93a27df1c861 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { useUiFilters } from '../context/UrlParamsContext'; +import { useParams } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; +import { useUiFilters } from '../context/UrlParamsContext'; +import { IUrlParams } from '../context/UrlParamsContext/types'; +import { useFetcher } from './useFetcher'; const INITIAL_DATA = { buckets: [] as TransactionDistributionAPIResponse['buckets'], @@ -17,8 +18,8 @@ const INITIAL_DATA = { }; export function useTransactionDistribution(urlParams: IUrlParams) { + const { serviceName } = useParams<{ serviceName?: string }>(); const { - serviceName, start, end, transactionType, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index a5d8275a25350..51ac6673251fb 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -37,6 +37,7 @@ import { import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; +import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -57,6 +58,7 @@ export interface ApmPluginStartDeps { home: void; licensing: void; triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; + embeddable: EmbeddableStart; } export class ApmPlugin implements Plugin { @@ -128,12 +130,18 @@ export class ApmPlugin implements Plugin { async mount(params: AppMountParameters) { // Load application bundle and Get start service - const [{ renderApp }, [coreStart]] = await Promise.all([ + const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ import('./application/csmApp'), core.getStartServices(), ]); - return renderApp(coreStart, pluginSetupDeps, params, config); + return renderApp( + coreStart, + pluginSetupDeps, + params, + config, + corePlugins as ApmPluginStartDeps + ); }, }); } diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts index 8b3ed38e25319..4e306c93805d0 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts @@ -51,7 +51,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', @@ -82,7 +82,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', @@ -109,7 +109,7 @@ describe('Observability dashboard data', () => { ); const response = await fetchOverviewPageData(params); expect(response).toEqual({ - appLink: '/app/apm#/services?rangeFrom=now-15m&rangeTo=now', + appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { services: { type: 'number', diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts index a20f89fac2d60..422c7b882e5dc 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts @@ -32,7 +32,7 @@ export const fetchOverviewPageData = async ({ const { serviceCount, transactionCoordinates } = data; return { - appLink: `/app/apm#/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, + appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, stats: { services: { type: 'number', diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json index 64602bc6b2769..f1643608496ad 100644 --- a/x-pack/plugins/apm/scripts/tsconfig.json +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "include": [ - "./**/*" + "./**/*", + "../observability" ], "exclude": [], "compilerOptions": { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index e7eb7b8de65e3..93af51b572aa5 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -81,6 +81,11 @@ export function registerTransactionDurationAnomalyAlertType({ anomalyDetectors, alertParams.environment ); + + if (mlJobIds.length === 0) { + return {}; + } + const anomalySearchParams = { body: { size: 0, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 75b0471424e79..5b78d97d5b681 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -7,22 +7,23 @@ import moment from 'moment'; // @ts-expect-error import { calculateAuto } from './calculate_auto'; -// @ts-expect-error -import { unitToSeconds } from './unit_to_seconds'; -export function getBucketSize(start: number, end: number, interval: string) { +export function getBucketSize( + start: number, + end: number, + numBuckets: number = 100 +) { const duration = moment.duration(end - start, 'ms'); - const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1); + const bucketSize = Math.max( + calculateAuto.near(numBuckets, duration).asSeconds(), + 1 + ); const intervalString = `${bucketSize}s`; - const matches = interval && interval.match(/^([\d]+)([shmdwMy]|ms)$/); - const minBucketSize = matches - ? Number(matches[1]) * unitToSeconds(matches[2]) - : 0; - if (bucketSize < minBucketSize) { + if (bucketSize < 0) { return { - bucketSize: minBucketSize, - intervalString: interval, + bucketSize: 0, + intervalString: 'auto', }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index 9f5b5cdf47552..ea018868f9517 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams( end: number, metricsInterval: number ) { - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); return { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6b69e57389dff..eba75433a5148 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,6 +5,7 @@ */ import moment from 'moment'; +import { isActivePlatinumLicense } from '../../../common/service_map'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { KibanaRequest } from '../../../../../../src/core/server'; import { APMConfig } from '../..'; @@ -98,11 +99,14 @@ export async function setupRequest( context, request, }), - ml: getMlSetup( - context.plugins.ml, - context.core.savedObjects.client, - request - ), + ml: + context.plugins.ml && isActivePlatinumLicense(context.licensing.license) + ? getMlSetup( + context.plugins.ml, + context.core.savedObjects.client, + request + ) + : undefined, config, }; @@ -115,14 +119,10 @@ export async function setupRequest( } function getMlSetup( - ml: APMRequestHandlerContext['plugins']['ml'], + ml: Required['ml'], savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], request: KibanaRequest ) { - if (!ml) { - return; - } - return { mlSystem: ml.mlSystemProvider(request), anomalyDetectors: ml.anomalyDetectorsProvider(request), diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 551384da2cca7..d7e64bdcacd12 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -44,7 +44,7 @@ export async function fetchAndTransformGcMetrics({ }) { const { start, end, apmEventClient, config } = setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const projection = getMetricsProjection({ setup, @@ -74,7 +74,7 @@ export async function fetchAndTransformGcMetrics({ field: `${LABEL_NAME}`, }, aggs: { - over_time: { + timeseries: { date_histogram: getMetricsDateHistogramParams( start, end, @@ -123,7 +123,7 @@ export async function fetchAndTransformGcMetrics({ const series = aggregations.per_pool.buckets.map((poolBucket, i) => { const label = poolBucket.key as string; - const timeseriesData = poolBucket.over_time; + const timeseriesData = poolBucket.timeseries; const data = timeseriesData.buckets.map((bucket) => { // derivative/value will be undefined for the first hit and if the `max` value is null diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index ec274d20b6005..ed8ae923e6e6c 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from 'kibana/server'; import Boom from 'boom'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; @@ -27,11 +26,9 @@ export type ServiceAnomaliesResponse = PromiseReturnType< export async function getServiceAnomalies({ setup, - logger, environment, }: { setup: Setup & SetupTimeRange; - logger: Logger; environment?: string; }) { const { ml, start, end } = setup; @@ -41,11 +38,20 @@ export async function getServiceAnomalies({ } const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const mlJobIds = await getMLJobIds(ml.anomalyDetectors, environment); + + if (!mlJobIds.length) { + return { + mlJobIds: [], + serviceAnomalies: {}, + }; + } + const params = { body: { size: 0, @@ -120,7 +126,9 @@ interface ServiceAnomaliesAggResponse { function transformResponseToServiceAnomalies( response: ServiceAnomaliesAggResponse ): Record { - const serviceAnomaliesMap = response.aggregations.services.buckets.reduce( + const serviceAnomaliesMap = ( + response.aggregations?.services.buckets ?? [] + ).reduce( (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { return { ...statsByServiceName, @@ -153,7 +161,7 @@ export async function getMLJobIds( (job) => job.custom_settings?.job_tags?.environment === environment ); if (!matchingMLJob) { - throw new Error(`ML job Not Found for environment "${environment}".`); + return []; } return [matchingMLJob.job_id]; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index d1c99d778c8f0..1e26b6f3f58f9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -58,6 +58,9 @@ describe('getServiceMapServiceNodeInfo', () => { indices: {}, start: 1593460053026000, end: 1593497863217000, + config: { + 'xpack.apm.metricsInterval': 30, + }, } as unknown) as Setup & SetupTimeRange; const environment = 'test environment'; const serviceName = 'test service name'; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index ca86c1d93fa6e..c5e072e073992 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -105,6 +105,24 @@ Array [ "field": "transaction.duration.us", }, }, + "timeseries": Object { + "aggs": Object { + "average": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, }, "terms": Object { "field": "service.name", @@ -194,6 +212,19 @@ Array [ "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, @@ -226,12 +257,37 @@ Array [ Object { "apm": Object { "events": Array [ - "error", + "transaction", ], }, "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, + "timeseries": Object { + "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, @@ -255,6 +311,14 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, + Object { + "terms": Object { + "event.outcome": Array [ + "failure", + "success", + ], + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index d888b43b63fac..50a968467fb4b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -15,15 +15,22 @@ import { getTransactionDurationAverages, getAgentNames, getTransactionRates, - getErrorRates, + getTransactionErrorRates, getEnvironments, + getHealthStatuses, } from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; export type ServicesItemsSetup = Setup & SetupTimeRange & SetupUIFilters; export type ServicesItemsProjection = ReturnType; -export async function getServicesItems(setup: ServicesItemsSetup) { +export async function getServicesItems({ + setup, + mlAnomaliesEnvironment, +}: { + setup: ServicesItemsSetup; + mlAnomaliesEnvironment?: string; +}) { const params = { projection: getServicesProjection({ setup }), setup, @@ -33,22 +40,25 @@ export async function getServicesItems(setup: ServicesItemsSetup) { transactionDurationAverages, agentNames, transactionRates, - errorRates, + transactionErrorRates, environments, + healthStatuses, ] = await Promise.all([ getTransactionDurationAverages(params), getAgentNames(params), getTransactionRates(params), - getErrorRates(params), + getTransactionErrorRates(params), getEnvironments(params), + getHealthStatuses(params, mlAnomaliesEnvironment), ]); const allMetrics = [ ...transactionDurationAverages, ...agentNames, ...transactionRates, - ...errorRates, + ...transactionErrorRates, ...environments, + ...healthStatuses, ]; return joinByKey(allMetrics, 'serviceName'); diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index ddce3b667a603..ab6b61ca21746 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EventOutcome } from '../../../../common/event_outcome'; +import { getSeverity } from '../../../../common/anomaly_detection'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, + EVENT_OUTCOME, } from '../../../../common/elasticsearch_fieldnames'; import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -15,6 +19,21 @@ import { ServicesItemsSetup, ServicesItemsProjection, } from './get_services_items'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getMLJobIds, + getServiceAnomalies, +} from '../../service_map/get_service_anomalies'; +import { AggregationResultOf } from '../../../../typings/elasticsearch/aggregations'; + +function getDateHistogramOpts(start: number, end: number) { + return { + field: '@timestamp', + fixed_interval: getBucketSize(start, end, 20).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }; +} const MAX_NUMBER_OF_SERVICES = 500; @@ -30,7 +49,7 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { @@ -51,6 +70,16 @@ export const getTransactionDurationAverages = async ({ field: TRANSACTION_DURATION, }, }, + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + aggs: { + average: { + avg: { + field: TRANSACTION_DURATION, + }, + }, + }, + }, }, }, }, @@ -64,9 +93,15 @@ export const getTransactionDurationAverages = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - avgResponseTime: bucket.average.value, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + avgResponseTime: { + value: serviceBucket.average.value, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.average.value, + })), + }, })); }; @@ -112,9 +147,10 @@ export const getAgentNames = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent + .name as AgentName, })); }; @@ -122,7 +158,7 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { @@ -136,6 +172,11 @@ export const getTransactionRates = async ({ ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + }, + }, }, }, }, @@ -150,33 +191,67 @@ export const getTransactionRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); - return aggregations.services.buckets.map((bucket) => { - const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; + return aggregations.services.buckets.map((serviceBucket) => { + const transactionsPerMinute = serviceBucket.doc_count / deltaAsMinutes; return { - serviceName: bucket.key as string, - transactionsPerMinute, + serviceName: serviceBucket.key as string, + transactionsPerMinute: { + value: transactionsPerMinute, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count / deltaAsMinutes, + })), + }, }; }); }; -export const getErrorRates = async ({ +export const getTransactionErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; + + const outcomes = { + terms: { + field: EVENT_OUTCOME, + }, + }; + const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.error], + events: [ProcessorEvent.transaction], }, body: { size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + terms: { + [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], + }, + }, + ], + }, + }, aggs: { services: { terms: { ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + outcomes, + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + aggs: { + outcomes, + }, + }, + }, }, }, }, @@ -189,13 +264,36 @@ export const getErrorRates = async ({ return []; } - const deltaAsMinutes = getDeltaAsMinutes(setup); + function calculateTransactionErrorPercentage( + outcomeResponse: AggregationResultOf + ) { + const successfulTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.success + )?.doc_count ?? 0; + const failedTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.failure + )?.doc_count ?? 0; - return aggregations.services.buckets.map((bucket) => { - const errorsPerMinute = bucket.doc_count / deltaAsMinutes; + return failedTransactions / (successfulTransactions + failedTransactions); + } + + return aggregations.services.buckets.map((serviceBucket) => { + const transactionErrorRate = calculateTransactionErrorPercentage( + serviceBucket.outcomes + ); return { - serviceName: bucket.key as string, - errorsPerMinute, + serviceName: serviceBucket.key as string, + transactionErrorRate: { + value: transactionErrorRate, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => { + return { + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }; + }), + }, }; }); }; @@ -241,8 +339,43 @@ export const getEnvironments = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - environments: bucket.environments.buckets.map((env) => env.key as string), + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + environments: serviceBucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), })); }; + +export const getHealthStatuses = async ( + { setup }: AggregationParams, + mlAnomaliesEnvironment?: string +) => { + if (!setup.ml) { + return []; + } + + const jobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + mlAnomaliesEnvironment + ); + if (!jobIds.length) { + return []; + } + + const anomalies = await getServiceAnomalies({ + setup, + environment: mlAnomaliesEnvironment, + }); + + return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { + const stats = anomalies.serviceAnomalies[serviceName]; + + const severity = getSeverity(stats.anomalyScore); + + return { + serviceName, + severity, + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 5a909ebd6ec54..28b4c64a4af47 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -17,11 +17,15 @@ import { getServicesItems } from './get_services_items'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServices( - setup: Setup & SetupTimeRange & SetupUIFilters -) { +export async function getServices({ + setup, + mlAnomaliesEnvironment, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + mlAnomaliesEnvironment?: string; +}) { const [items, hasLegacyData] = await Promise.all([ - getServicesItems(setup), + getServicesItems({ setup, mlAnomaliesEnvironment }), getLegacyDataStatus(setup), ]); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 99c58a17d396a..9b0dd7a03ca5b 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -38,7 +38,7 @@ describe('services queries', () => { }); it('fetches the service items', async () => { - mock = await inspectSearchParams((setup) => getServicesItems(setup)); + mock = await inspectSearchParams((setup) => getServicesItems({ setup })); const allParams = mock.spy.mock.calls.map((call) => call[0]); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index f7b7f72168160..1e08b04416e17 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -62,7 +62,7 @@ export async function getErrorRate({ total_transactions: { date_histogram: { field: '@timestamp', - fixed_interval: getBucketSize(start, end, 'auto').intervalString, + fixed_interval: getBucketSize(start, end).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index f68082dfaa1e1..51118278fb824 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -24,7 +24,7 @@ export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 596c3137ec19f..d8865f0049d35 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -64,16 +64,10 @@ export async function getAnomalySeries({ return; } - let mlJobIds: string[] = []; - try { - mlJobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - uiFilters.environment - ); - } catch (error) { - logger.error(error); - return; - } + const mlJobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + uiFilters.environment + ); // don't fetch anomalies if there are isn't exaclty 1 ML job match for the given environment if (mlJobIds.length !== 1) { @@ -87,7 +81,7 @@ export async function getAnomalySeries({ } const { start, end } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); + const { intervalString, bucketSize } = getBucketSize(start, end); const esResponse = await anomalySeriesFetcher({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 1498c22e327d6..f39529b59caa6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -35,7 +35,7 @@ export function timeseriesFetcher({ setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, apmEventClient } = setup; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 8a0fe1a57736f..ea06bd57bfff2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -20,7 +20,7 @@ export async function getApmTimeseriesData(options: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end } = options.setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 971e247d98986..8533d54ed6277 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import * as t from 'io-ts'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; @@ -33,7 +33,7 @@ export const serviceMapRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); @@ -59,7 +59,7 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } const logger = context.logger; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 74ab717b8de59..cc7f25867df2c 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,6 +16,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -23,8 +24,17 @@ export const servicesRoute = createRoute(() => ({ query: t.intersection([uiFiltersRt, rangeRt]), }, handler: async ({ context, request }) => { + const { environment } = getParsedUiFilters({ + uiFilters: context.params.query.uiFilters, + logger: context.logger, + }); + const setup = await setupRequest(context, request); - const services = await getServices(setup); + + const services = await getServices({ + setup, + mlAnomaliesEnvironment: environment, + }); return services; }, diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index ac25f22751f2f..290e81bd29973 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -6,6 +6,7 @@ import * as t from 'io-ts'; import Boom from 'boom'; +import { isActivePlatinumLicense } from '../../../common/service_map'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; @@ -24,8 +25,7 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } @@ -56,8 +56,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 7a7592b248960..bbd2c9eb86249 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -346,6 +346,12 @@ export type ValidAggregationKeysOf< T extends Record > = keyof (UnionToIntersection extends never ? T : UnionToIntersection); +export type AggregationResultOf< + TAggregationOptionsMap extends AggregationOptionsMap, + TDocument +> = AggregationResponsePart[AggregationType & + ValidAggregationKeysOf]; + export type AggregationResponseMap< TAggregationInputMap extends AggregationInputMap | undefined, TDocument diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts new file mode 100644 index 0000000000000..6f2aec757eb37 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./', () => ({ + FlashMessagesLogic: { + actions: { + setFlashMessages: jest.fn(), + setQueuedMessages: jest.fn(), + }, + }, +})); +import { FlashMessagesLogic } from './'; + +import { flashAPIErrors } from './handle_api_errors'; + +describe('flashAPIErrors', () => { + const mockHttpError = { + body: { + statusCode: 404, + error: 'Not Found', + message: 'Could not find X,Could not find Y,Something else bad happened', + attributes: { + errors: ['Could not find X', 'Could not find Y', 'Something else bad happened'], + }, + }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('converts API errors into flash messages', () => { + flashAPIErrors(mockHttpError); + + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'Could not find X' }, + { type: 'error', message: 'Could not find Y' }, + { type: 'error', message: 'Something else bad happened' }, + ]); + }); + + it('queues messages when isQueued is passed', () => { + flashAPIErrors(mockHttpError, { isQueued: true }); + + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'Could not find X' }, + { type: 'error', message: 'Could not find Y' }, + { type: 'error', message: 'Something else bad happened' }, + ]); + }); + + it('displays a generic error message and re-throws non-API errors', () => { + try { + flashAPIErrors(Error('whatever') as any); + } catch (e) { + expect(e.message).toEqual('whatever'); + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + { type: 'error', message: 'An unexpected error occurred' }, + ]); + } + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts new file mode 100644 index 0000000000000..5e56c4fb0bd22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpResponse } from 'src/core/public'; + +import { FlashMessagesLogic, IFlashMessage } from './'; + +/** + * The API errors we are handling can come from one of two ways: + * - When our http calls recieve a response containing an error code, such as a 404 or 500 + * - Our own JS while handling a successful response + * + * In the first case, if it is a purposeful error (like a 404) we will receive an + * `errors` property in the response's data, which will contain messages we can + * display to the user. + */ +interface IErrorResponse { + statusCode: number; + error: string; + message: string; + attributes: { + errors: string[]; + }; +} +interface IOptions { + isQueued?: boolean; +} + +/** + * Converts API/HTTP errors into user-facing Flash Messages + */ +export const flashAPIErrors = ( + error: HttpResponse, + { isQueued }: IOptions = {} +) => { + const defaultErrorMessage = 'An unexpected error occurred'; + + const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors) + ? error.body!.attributes.errors.map((message) => ({ type: 'error', message })) + : [{ type: 'error', message: defaultErrorMessage }]; + + if (isQueued) { + FlashMessagesLogic.actions.setQueuedMessages(errorFlashMessages); + } else { + FlashMessagesLogic.actions.setFlashMessages(errorFlashMessages); + } + + // If this was a programming error or a failed request (such as a CORS) error, + // we rethrow the error so it shows up in the developer console + if (!error?.body?.message) { + throw error; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index a6957340d33d3..c032e3b04ebe6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -74,21 +74,24 @@ describe('HttpLogic', () => { describe('errorConnectingInterceptor', () => { it('handles errors connecting to Enterprise Search', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/app_search/engines', status: 502 } }); + const httpResponse = { response: { url: '/api/app_search/engines', status: 502 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled(); }); it('does not handle non-502 Enterprise Search errors', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/workplace_search/overview', status: 404 } }); + const httpResponse = { response: { url: '/api/workplace_search/overview', status: 404 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); it('does not handle errors for unrelated calls', async () => { const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - await responseError({ response: { url: '/api/some_other_plugin/', status: 502 } }); + const httpResponse = { response: { url: '/api/some_other_plugin/', status: 502 } }; + await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index fb2d9b1061723..ec9db30ddef3b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -6,7 +6,7 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpSetup } from 'src/core/public'; +import { HttpSetup, HttpInterceptorResponseError } from 'src/core/public'; export interface IHttpValues { http: HttpSetup; @@ -68,7 +68,9 @@ export const HttpLogic = kea>({ if (isApiResponse && hasErrorConnecting) { actions.setErrorConnecting(true); } - return httpResponse; + + // Re-throw error so that downstream catches work as expected + return Promise.reject(httpResponse) as Promise; }, }); httpInterceptors.push(errorConnectingInterceptor); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 34f83ef3a3fd2..0c1e81e3aba46 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -30,109 +30,198 @@ describe('EnterpriseSearchRequestHandler', () => { fetchMock.mockReset(); }); - it('makes an API call and returns the response', async () => { - const responseBody = { - results: [{ name: 'engine1' }], - meta: { page: { total_results: 1 } }, - }; + describe('createRequest()', () => { + it('makes an API call and returns the response', async () => { + const responseBody = { + results: [{ name: 'engine1' }], + meta: { page: { total_results: 1 } }, + }; - EnterpriseSearchAPI.mockReturn(responseBody); + EnterpriseSearchAPI.mockReturn(responseBody); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/collection', - }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/collection', + }); - await makeAPICall(requestHandler, { - query: { - type: 'indexed', - pageIndex: 1, - }, - }); + await makeAPICall(requestHandler, { + query: { + type: 'indexed', + pageIndex: 1, + }, + }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith( - 'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1', - { method: 'GET' } - ); + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1', + { method: 'GET' } + ); - expect(responseMock.custom).toHaveBeenCalledWith({ - body: responseBody, - statusCode: 200, + expect(responseMock.custom).toHaveBeenCalledWith({ + body: responseBody, + statusCode: 200, + }); }); - }); - describe('request passing', () => { - it('passes route method', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); + describe('request passing', () => { + it('passes route method', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + + await makeAPICall(requestHandler, { route: { method: 'POST' } }); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + method: 'POST', + }); - await makeAPICall(requestHandler, { route: { method: 'POST' } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - method: 'POST', + await makeAPICall(requestHandler, { route: { method: 'DELETE' } }); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + method: 'DELETE', + }); }); - await makeAPICall(requestHandler, { route: { method: 'DELETE' } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - method: 'DELETE', + it('passes request body', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + await makeAPICall(requestHandler, { body: { bodacious: true } }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + body: '{"bodacious":true}', + }); }); - }); - it('passes request body', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); - await makeAPICall(requestHandler, { body: { bodacious: true } }); + it('passes custom params set by the handler, which override request params', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + params: { someQuery: true }, + }); + await makeAPICall(requestHandler, { query: { someQuery: false } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { - body: '{"bodacious":true}', + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/api/example?someQuery=true' + ); }); }); - it('passes custom params set by the handler, which override request params', async () => { - const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/api/example', - params: { someQuery: true }, + describe('response passing', () => { + it('returns the response status code from Enterprise Search', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 201 }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + await makeAPICall(requestHandler); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); + expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 201 }); }); - await makeAPICall(requestHandler, { query: { someQuery: false } }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith( - 'http://localhost:3002/api/example?someQuery=true' - ); + // TODO: It's possible we may also pass back headers at some point + // from Enterprise Search, e.g. the x-read-only mode header }); }); - describe('response passing', () => { - it('returns the response status code from Enterprise Search', async () => { - EnterpriseSearchAPI.mockReturn({}, { status: 404 }); + describe('error responses', () => { + describe('handleClientError()', () => { + afterEach(() => { + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/4xx'); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example' }); - await makeAPICall(requestHandler); + it('passes back json.error', async () => { + const error = 'some error message'; + EnterpriseSearchAPI.mockReturn({ error }, { status: 404, headers: JSON_HEADER }); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); - expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 404 }); - }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); - // TODO: It's possible we may also pass back headers at some point - // from Enterprise Search, e.g. the x-read-only mode header - }); + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { + message: 'some error message', + attributes: { errors: ['some error message'] }, + }, + }); + }); - describe('error handling', () => { - afterEach(() => { - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Error connecting to Enterprise Search') - ); + it('passes back json.errors', async () => { + const errors = ['one', 'two', 'three']; + EnterpriseSearchAPI.mockReturn({ errors }, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'one,two,three', + attributes: { errors: ['one', 'two', 'three'] }, + }, + }); + }); + + it('handles empty json', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'Bad Request', + attributes: { errors: ['Bad Request'] }, + }, + }); + }); + + it('handles invalid json', async () => { + EnterpriseSearchAPI.mockReturn('invalid' as any, { status: 400, headers: JSON_HEADER }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { + message: 'Bad Request', + attributes: { errors: ['Bad Request'] }, + }, + }); + }); + + it('handles blank bodies', async () => { + EnterpriseSearchAPI.mockReturn(undefined as any, { status: 404 }); + + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/4xx' }); + await makeAPICall(requestHandler); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { + message: 'Not Found', + attributes: { errors: ['Not Found'] }, + }, + }); + }); }); - it('returns an error when an API request fails', async () => { - EnterpriseSearchAPI.mockReturnError(); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/failed' }); + it('handleServerError()', async () => { + EnterpriseSearchAPI.mockReturn('something crashed!' as any, { status: 500 }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/5xx' }); await makeAPICall(requestHandler); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/failed'); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/5xx'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Failed', statusCode: 502, + body: expect.stringContaining('Enterprise Search encountered an internal server error'), }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Enterprise Search Server Error 500 at : "something crashed!"' + ); }); - it('returns an error when `hasValidData` fails', async () => { + it('handleInvalidDataError()', async () => { EnterpriseSearchAPI.mockReturn({ results: false }); const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/invalid', @@ -143,15 +232,29 @@ describe('EnterpriseSearchRequestHandler', () => { EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/invalid'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Invalid data received', statusCode: 502, + body: 'Invalid data received from Enterprise Search', }); - expect(mockLogger.debug).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Invalid data received from : {"results":false}' ); }); - describe('user authentication errors', () => { + it('handleConnectionError()', async () => { + EnterpriseSearchAPI.mockReturnError(); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/failed' }); + + await makeAPICall(requestHandler); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/failed'); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: 'Error connecting to Enterprise Search: Failed', + }); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + describe('handleAuthenticationError()', () => { afterEach(async () => { const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/unauthenticated', @@ -160,9 +263,10 @@ describe('EnterpriseSearchRequestHandler', () => { EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/unauthenticated'); expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Cannot authenticate Enterprise Search user', statusCode: 502, + body: 'Cannot authenticate Enterprise Search user', }); + expect(mockLogger.error).toHaveBeenCalled(); }); it('errors when redirected to /login', async () => { @@ -175,7 +279,7 @@ describe('EnterpriseSearchRequestHandler', () => { }); }); - it('has a helper for checking empty objects', async () => { + it('isEmptyObj', async () => { expect(enterpriseSearchRequestHandler.isEmptyObj({})).toEqual(true); expect(enterpriseSearchRequestHandler.isEmptyObj({ empty: false })).toEqual(false); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 18f10c590847c..00d5eaf5d6a83 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import fetch from 'node-fetch'; +import fetch, { Response } from 'node-fetch'; import querystring from 'querystring'; import { RequestHandler, @@ -25,6 +25,12 @@ interface IRequestParams { params?: object; hasValidData?: (body?: ResponseBody) => boolean; } +interface IErrorResponse { + message: string; + attributes: { + errors: string[]; + }; +} export interface IEnterpriseSearchRequestHandler { createRequest(requestParams?: object): RequestHandler; } @@ -71,34 +77,131 @@ export class EnterpriseSearchRequestHandler { ? JSON.stringify(request.body) : undefined; - // Call the Enterprise Search API and pass back response to the front-end + // Call the Enterprise Search API const apiResponse = await fetch(url, { method, headers, body }); + // Handle authentication redirects if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { - throw new Error('Cannot authenticate Enterprise Search user'); + return this.handleAuthenticationError(response); } + // Handle 400-500+ responses from the Enterprise Search server const { status } = apiResponse; - const json = await apiResponse.json(); + if (status >= 500) { + return this.handleServerError(response, apiResponse, url); + } else if (status >= 400) { + return this.handleClientError(response, apiResponse); + } - if (hasValidData(json)) { - return response.custom({ statusCode: status, body: json }); - } else { - this.log.debug(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); - throw new Error('Invalid data received'); + // Check returned data + const json = await apiResponse.json(); + if (!hasValidData(json)) { + return this.handleInvalidDataError(response, url, json); } + + // Pass successful responses back to the front-end + return response.custom({ statusCode: status, body: json }); } catch (e) { - const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + // Catch connection/auth errors + return this.handleConnectionError(response, e); + } + }; + } - this.log.error(errorMessage); - if (e instanceof Error) this.log.debug(e.stack as string); + /** + * Attempt to grab a usable error body from Enterprise Search - this isn't + * always possible because some of our internal endpoints send back blank + * bodies, and sometimes the server sends back Ruby on Rails error pages + */ + async getErrorResponseBody(apiResponse: Response) { + const { statusText } = apiResponse; + const contentType = apiResponse.headers.get('content-type') || ''; - return response.customError({ statusCode: 502, body: errorMessage }); - } + // Default response + let body: IErrorResponse = { + message: statusText, + attributes: { errors: [statusText] }, }; + + try { + if (contentType.includes('application/json')) { + // Try parsing body as JSON + const json = await apiResponse.json(); + + // Some of our internal endpoints return either an `error` or `errors` key, + // which can both return either a string or array of strings ¯\_(ツ)_/¯ + const errors = json.error || json.errors || [statusText]; + body = { + message: errors.toString(), + attributes: { errors: Array.isArray(errors) ? errors : [errors] }, + }; + } else { + // Try parsing body as text/html + const text = await apiResponse.text(); + if (text) { + body = { + message: text, + attributes: { errors: [text] }, + }; + } + } + } catch { + // Fail silently + } + + return body; + } + + /** + * Error response helpers + */ + + async handleClientError(response: KibanaResponseFactory, apiResponse: Response) { + const { status } = apiResponse; + const body = await this.getErrorResponseBody(apiResponse); + + return response.customError({ statusCode: status, body }); + } + + async handleServerError(response: KibanaResponseFactory, apiResponse: Response, url: string) { + const { status } = apiResponse; + const { message } = await this.getErrorResponseBody(apiResponse); + + // Don't expose server errors to the front-end, as they may contain sensitive stack traces + const errorMessage = + 'Enterprise Search encountered an internal server error. Please contact your system administrator if the problem persists.'; + + this.log.error(`Enterprise Search Server Error ${status} at <${url}>: ${message}`); + return response.customError({ statusCode: 502, body: errorMessage }); + } + + handleInvalidDataError(response: KibanaResponseFactory, url: string, json: object) { + const errorMessage = 'Invalid data received from Enterprise Search'; + + this.log.error(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); + return response.customError({ statusCode: 502, body: errorMessage }); } - // Small helper + handleConnectionError(response: KibanaResponseFactory, e: Error) { + const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + + this.log.error(errorMessage); + if (e instanceof Error) this.log.debug(e.stack as string); + + return response.customError({ statusCode: 502, body: errorMessage }); + } + + handleAuthenticationError(response: KibanaResponseFactory) { + const errorMessage = 'Cannot authenticate Enterprise Search user'; + + this.log.error(errorMessage); + return response.customError({ statusCode: 502, body: errorMessage }); + } + + /** + * Misc helpers + */ + isEmptyObj(obj: object) { return Object.keys(obj).length === 0; } diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 12cf7ccac6c59..20b1e0878a730 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -103,6 +103,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadComponentTemplatesResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/component_templates`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -114,6 +125,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setCreateTemplateResponse, setUpdateTemplateResponse, setSimulateTemplateResponse, + setLoadComponentTemplatesResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 910d9be842da8..e221c3d421e02 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -24,7 +24,11 @@ import { ExtensionsService } from '../../../public/services'; import { UiMetricService } from '../../../public/application/services/ui_metric'; import { setUiMetricService } from '../../../public/application/services/api'; import { setExtensionsService } from '../../../public/application/store/selectors'; -import { MappingsEditorProvider } from '../../../public/application/components'; +import { + MappingsEditorProvider, + ComponentTemplatesProvider, +} from '../../../public/application/components'; +import { componentTemplatesMockDependencies } from '../../../public/application/components/component_templates/__jest__'; import { init as initHttpRequests } from './http_requests'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -34,9 +38,11 @@ export const services = { extensionsService: new ExtensionsService(), uiMetricService: new UiMetricService('index_management'), }; + services.uiMetricService.setup({ reportUiStats() {} } as any); setExtensionsService(services.extensionsService); setUiMetricService(services.uiMetricService); + const appDependencies = { services, core: { getUrlForApp: () => {} }, @@ -66,9 +72,11 @@ export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) return ( - - - + + + + + ); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts index 3f6e5d7d4dab2..ef5cffc05d8d7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/constants.ts @@ -26,7 +26,5 @@ export const ALIASES = { }; export const MAPPINGS = { - _source: {}, - _meta: {}, properties: {}, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx index ccb729db44f9b..8b74e9fb0cdf8 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { getTemplate } from '../../../test/fixtures'; -import { setupEnvironment, nextTick } from '../helpers'; +import { getComposableTemplate } from '../../../test/fixtures'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, MAPPINGS } from './constants'; import { setup } from './template_clone.helpers'; @@ -41,32 +41,35 @@ jest.mock('@elastic/eui', () => { }; }); -// FLAKY: https://github.com/elastic/kibana/issues/59849 -describe.skip('', () => { +const templateToClone = getComposableTemplate({ + name: TEMPLATE_NAME, + indexPatterns: ['indexPattern1'], + template: { + mappings: MAPPINGS, + }, +}); + +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); - afterAll(() => { - server.restore(); + beforeAll(() => { + jest.useFakeTimers(); + httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]); + httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone); }); - const templateToClone = getTemplate({ - name: TEMPLATE_NAME, - indexPatterns: ['indexPattern1'], - template: { - mappings: MAPPINGS, - }, - isLegacy: true, + afterAll(() => { + server.restore(); + jest.useRealTimers(); }); beforeEach(async () => { - httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone); - await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -80,30 +83,26 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - await act(async () => { - // Complete step 1 (logistics) - // Specify index patterns, but do not change name (keep default) - await actions.completeStepOne({ - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Bypass step 2 (index settings) - await actions.completeStepTwo(); - - // Bypass step 3 (mappings) - await actions.completeStepThree(); - - // Bypass step 4 (aliases) - await actions.completeStepFour(); + // Logistics + // Specify index patterns, but do not change name (keep default) + await actions.completeStepOne({ + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); }); it('should send the correct payload', async () => { const { actions } = testBed; await act(async () => { - actions.clickSubmitButton(); - await nextTick(); + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -113,6 +112,8 @@ describe.skip('', () => { name: `${templateToClone.name}-copy`, indexPatterns: DEFAULT_INDEX_PATTERNS, }; + // @ts-expect-error + delete expected.template; // As no settings, mappings or aliases have been defined, no "template" param is sent expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 76b6c34f999d5..b67e503f8d3e2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -7,12 +7,11 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, SETTINGS, - MAPPINGS, ALIASES, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, } from './constants'; @@ -61,21 +60,50 @@ const KEYWORD_MAPPING_FIELD = { type: 'keyword', }; -// FLAKY: https://github.com/elastic/kibana/issues/67833 -describe.skip('', () => { +const componentTemplate1 = { + name: 'test_component_template_1', + hasMappings: true, + hasAliases: false, + hasSettings: false, + usedBy: [], + isManaged: false, +}; + +const componentTemplate2 = { + name: 'test_component_template_2', + hasMappings: false, + hasAliases: false, + hasSettings: true, + usedBy: ['test_index_template_1'], + isManaged: false, +}; + +const componentTemplates = [componentTemplate1, componentTemplate2]; + +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); + beforeAll(() => { + jest.useFakeTimers(); + + httpRequestsMockHelpers.setLoadComponentTemplatesResponse(componentTemplates); + + // disable all react-beautiful-dnd development warnings + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = true; + }); + afterAll(() => { server.restore(); + jest.useRealTimers(); + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = false; }); describe('on component mount', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); }); @@ -93,9 +121,8 @@ describe.skip('', () => { await act(async () => { actions.clickNextButton(); - await nextTick(); - component.update(); }); + component.update(); expect(find('nextButton').props().disabled).toEqual(true); }); @@ -105,21 +132,131 @@ describe.skip('', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); - describe('index settings (step 2)', () => { + describe('component templates (step 2)', () => { beforeEach(async () => { const { actions } = testBed; + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + }); + + it('should set the correct page title', async () => { + const { exists, find } = testBed; + + expect(exists('stepComponents')).toBe(true); + expect(find('stepTitle').text()).toEqual('Component templates (optional)'); + }); + + it('should list the available component templates', () => { + const { + actions: { + componentTemplates: { getComponentTemplatesInList }, + }, + } = testBed; + const componentsFound = getComponentTemplatesInList(); + expect(componentsFound).toEqual(componentTemplates.map((c) => c.name)); + }); + + it('should allow to search for a component', async () => { + const { + component, + form: { setInputValue }, + actions: { + componentTemplates: { getComponentTemplatesInList }, + }, + } = testBed; await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + setInputValue('componentTemplateSearchBox', 'template_2'); }); + component.update(); + + const componentsFound = getComponentTemplatesInList(); + expect(componentsFound).toEqual(['test_component_template_2']); }); - it('should set the correct page title', () => { + it('should allow to filter component by Index settings, mappings and aliases', async () => { + const { + find, + exists, + actions: { + componentTemplates: { showFilters, selectFilter, getComponentTemplatesInList }, + }, + } = testBed; + + showFilters(); + + expect(find('filterList.filterItem').map((wrapper) => wrapper.text())).toEqual([ + 'Index settings', + 'Mappings', + 'Aliases', + ]); + + await selectFilter('settings'); + expect(getComponentTemplatesInList()).toEqual(['test_component_template_2']); // only this one has settings + + await selectFilter('mappings'); + expect(exists('componentTemplatesList')).toBe(false); // no component has **both** settings and mappings + expect(exists('componentTemplates.emptySearchResult')).toBe(true); + expect(find('componentTemplates.emptySearchResult').text()).toContain( + 'No components match your search' + ); + + await selectFilter('settings'); // unselect settings + expect(getComponentTemplatesInList()).toEqual(['test_component_template_1']); // only this one has mappings + + await selectFilter('mappings'); // unselect mappings (back to start) + expect(getComponentTemplatesInList()).toEqual([ + 'test_component_template_1', + 'test_component_template_2', + ]); + + await selectFilter('aliases'); + expect(exists('componentTemplatesList')).toBe(false); // no component has aliases defined. + }); + + it('should allow to select and unselect a component template', async () => { + const { + find, + exists, + actions: { + componentTemplates: { + selectComponentAt, + unSelectComponentAt, + getComponentTemplatesSelected, + }, + }, + } = testBed; + + // Start with empty selection + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(true); + expect(find('componentTemplatesSelection.emptyPrompt').text()).toContain( + 'Add component template building blocks to this template.' + ); + + // Select first component in the list + await selectComponentAt(0); + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(false); + expect(getComponentTemplatesSelected()).toEqual(['test_component_template_1']); + + // Unselect the component + await unSelectComponentAt(0); + expect(exists('componentTemplatesSelection.emptyPrompt')).toBe(true); + }); + }); + + describe('index settings (step 3)', () => { + beforeEach(async () => { + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + }); + + it('should set the correct page title', async () => { const { exists, find } = testBed; expect(exists('stepSettings')).toBe(true); @@ -130,24 +267,22 @@ describe.skip('', () => { const { form, actions } = testBed; await act(async () => { - actions.completeStepTwo('{ invalidJsonString '); + actions.completeStepThree('{ invalidJsonString '); }); expect(form.getErrorsMessages()).toContain('Invalid JSON format.'); }); }); - describe('mappings (step 3)', () => { + describe('mappings (step 4)', () => { beforeEach(async () => { const { actions } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); - - // Complete step 2 (index settings) - await actions.completeStepTwo('{}'); - }); + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree('{}'); }); it('should set the correct page title', () => { @@ -160,11 +295,9 @@ describe.skip('', () => { it('should allow the user to define document fields for a mapping', async () => { const { actions, find } = testBed; - await act(async () => { - await actions.addMappingField('field_1', 'text'); - await actions.addMappingField('field_2', 'text'); - await actions.addMappingField('field_3', 'text'); - }); + await actions.mappings.addField('field_1', 'text'); + await actions.mappings.addField('field_2', 'text'); + await actions.mappings.addField('field_3', 'text'); expect(find('fieldsListItem').length).toBe(3); }); @@ -172,10 +305,8 @@ describe.skip('', () => { it('should allow the user to remove a document field from a mapping', async () => { const { actions, find } = testBed; - await act(async () => { - await actions.addMappingField('field_1', 'text'); - await actions.addMappingField('field_2', 'text'); - }); + await actions.mappings.addField('field_1', 'text'); + await actions.mappings.addField('field_2', 'text'); expect(find('fieldsListItem').length).toBe(2); @@ -187,20 +318,17 @@ describe.skip('', () => { }); }); - describe('aliases (step 4)', () => { + describe('aliases (step 5)', () => { beforeEach(async () => { const { actions } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); - - // Complete step 2 (index settings) - await actions.completeStepTwo('{}'); - - // Complete step 3 (mappings) - await actions.completeStepThree(); - }); + // Logistics + await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree('{}'); + // Mappings + await actions.completeStepFour(); }); it('should set the correct page title', () => { @@ -213,39 +341,35 @@ describe.skip('', () => { it('should not allow invalid json', async () => { const { actions, form } = testBed; - await act(async () => { - // Complete step 4 (aliases) with invalid json - await actions.completeStepFour('{ invalidJsonString ', false); - }); + // Complete step 5 (aliases) with invalid json + await actions.completeStepFive('{ invalidJsonString '); expect(form.getErrorsMessages()).toContain('Invalid JSON format.'); }); }); }); - describe('review (step 5)', () => { + describe('review (step 6)', () => { beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); - - // Complete step 3 (mappings) - await actions.completeStepThree(); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo('test_component_template_1'); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); }); it('should set the correct step title', () => { @@ -255,26 +379,30 @@ describe.skip('', () => { }); describe('tabs', () => { - test('should have 2 tabs', () => { + test('should have 3 tabs', () => { const { find } = testBed; - expect(find('summaryTabContent').find('.euiTab').length).toBe(2); + expect(find('summaryTabContent').find('.euiTab').length).toBe(3); expect( find('summaryTabContent') .find('.euiTab') .map((t) => t.text()) - ).toEqual(['Summary', 'Request']); + ).toEqual(['Summary', 'Preview', 'Request']); }); - test('should navigate to the Request tab', async () => { + test('should navigate to the preview and request tab', async () => { const { exists, actions } = testBed; expect(exists('summaryTab')).toBe(true); expect(exists('requestTab')).toBe(false); + expect(exists('previewTab')).toBe(false); - actions.selectSummaryTab('request'); - + await actions.review.selectTab('preview'); expect(exists('summaryTab')).toBe(false); + expect(exists('previewTab')).toBe(true); + + await actions.review.selectTab('request'); + expect(exists('previewTab')).toBe(false); expect(exists('requestTab')).toBe(true); }); }); @@ -282,24 +410,23 @@ describe.skip('', () => { it('should render a warning message if a wildcard is used as an index pattern', async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: ['*'], // Set wildcard index pattern - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify({})); - - // Complete step 3 (mappings) - await actions.completeStepThree(); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify({})); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: ['*'], // Set wildcard index pattern }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(JSON.stringify({})); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(JSON.stringify({})); const { exists, find } = testBed; @@ -316,32 +443,32 @@ describe.skip('', () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); - - const { actions } = testBed; - // Complete step 1 (logistics) - await actions.completeStepOne({ - name: TEMPLATE_NAME, - indexPatterns: DEFAULT_INDEX_PATTERNS, - }); - - // Complete step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); - - // Complete step 3 (mappings) - await actions.completeStepThree(MAPPING_FIELDS); + }); + testBed.component.update(); - // Complete step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: TEMPLATE_NAME, + indexPatterns: DEFAULT_INDEX_PATTERNS, }); + // Component templates + await actions.completeStepTwo('test_component_template_1'); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); + // Mappings + await actions.completeStepFour(MAPPING_FIELDS); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); }); it('should send the correct payload', async () => { - const { actions } = testBed; + const { actions, find } = testBed; + + expect(find('stepTitle').text()).toEqual(`Review details for '${TEMPLATE_NAME}'`); await act(async () => { - actions.clickSubmitButton(); - await nextTick(); + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -349,10 +476,10 @@ describe.skip('', () => { const expected = { name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, + composedOf: ['test_component_template_1'], template: { settings: SETTINGS, mappings: { - ...MAPPINGS, properties: { [BOOLEAN_MAPPING_FIELD.name]: { type: BOOLEAN_MAPPING_FIELD.type, @@ -370,6 +497,7 @@ describe.skip('', () => { _kbnMeta: { type: 'default', isLegacy: false, + hasDatastream: false, }, }; @@ -388,10 +516,9 @@ describe.skip('', () => { httpRequestsMockHelpers.setCreateTemplateResponse(undefined, { body: error }); await act(async () => { - actions.clickSubmitButton(); - await nextTick(); - component.update(); + actions.clickNextButton(); }); + component.update(); expect(exists('saveTemplateError')).toBe(true); expect(find('saveTemplateError').text()).toContain(error.message); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index de66013241236..37d489b6afe72 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import * as fixtures from '../../../test/fixtures'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './constants'; import { setup } from './template_edit.helpers'; @@ -52,51 +52,51 @@ jest.mock('@elastic/eui', () => { }; }); -// FLAKY: https://github.com/elastic/kibana/issues/65567 -describe.skip('', () => { +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); + beforeAll(() => { + jest.useFakeTimers(); + httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]); + }); + afterAll(() => { server.restore(); + jest.useRealTimers(); }); describe('without mappings', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], - isLegacy: true, }); - beforeEach(async () => { + beforeAll(() => { httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit); + }); - testBed = await setup(); - + beforeEach(async () => { await act(async () => { - await nextTick(); - testBed.component.update(); + testBed = await setup(); }); + + testBed.component.update(); }); it('allows you to add mappings', async () => { const { actions, find } = testBed; - - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne(); - - // Step 2 (index settings) - await actions.completeStepTwo(); - - // Step 3 (mappings) - await act(async () => { - await actions.addMappingField('field_1', 'text'); - }); - - expect(find('fieldsListItem').length).toBe(1); - }); + // Logistics + await actions.completeStepOne(); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.mappings.addField('field_1', 'text'); + + expect(find('fieldsListItem').length).toBe(1); }); }); @@ -107,16 +107,17 @@ describe.skip('', () => { template: { mappings: MAPPING, }, - isLegacy: true, }); - beforeEach(async () => { + beforeAll(() => { httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit); + }); + beforeEach(async () => { await act(async () => { testBed = await setup(); - await testBed.waitFor('templateForm'); }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -138,64 +139,64 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - await act(async () => { - // Complete step 1 (logistics) - await actions.completeStepOne({ - indexPatterns: UPDATED_INDEX_PATTERN, - }); - - // Step 2 (index settings) - await actions.completeStepTwo(JSON.stringify(SETTINGS)); + // Logistics + await actions.completeStepOne({ + indexPatterns: UPDATED_INDEX_PATTERN, + priority: 3, }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(JSON.stringify(SETTINGS)); }); it('should send the correct payload with changed values', async () => { - const { actions, component, find, form } = testBed; + const { actions, component, exists, form } = testBed; + // Make some changes to the mappings await act(async () => { - // Make some changes to the mappings (step 3) - actions.clickEditButtonAtField(0); // Select the first field to edit - await nextTick(); - component.update(); }); + component.update(); - // verify edit field flyout - expect(find('mappingsEditorFieldEdit').length).toEqual(1); + // Verify that the edit field flyout is opened + expect(exists('mappingsEditorFieldEdit')).toBe(true); + // Change the field name await act(async () => { - // change the field name form.setInputValue('nameParameterInput', UPDATED_MAPPING_TEXT_FIELD_NAME); + }); - // Save changes + // Save changes on the field + await act(async () => { actions.clickEditFieldUpdateButton(); - await nextTick(); - component.update(); + }); + component.update(); - // Proceed to the next step + // Proceed to the next step + await act(async () => { actions.clickNextButton(); - await nextTick(50); - component.update(); + }); + component.update(); - // Step 4 (aliases) - await actions.completeStepFour(JSON.stringify(ALIASES)); + // Aliases + await actions.completeStepFive(JSON.stringify(ALIASES)); - // Submit the form - actions.clickSubmitButton(); - await nextTick(); + // Submit the form + await act(async () => { + actions.clickNextButton(); }); const latestRequest = server.requests[server.requests.length - 1]; - const { version, order } = templateToEdit; + const { version } = templateToEdit; const expected = { name: TEMPLATE_NAME, version, - order, + priority: 3, indexPatterns: UPDATED_INDEX_PATTERN, template: { mappings: { - ...MAPPING, properties: { [UPDATED_MAPPING_TEXT_FIELD_NAME]: { type: 'text', @@ -215,6 +216,7 @@ describe.skip('', () => { _kbnMeta: { type: 'default', isLegacy: templateToEdit._kbnMeta.isLegacy, + hasDatastream: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index fdf837a914cf1..025410129a002 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; import { TemplateDeserialized } from '../../../common'; -import { nextTick } from '../helpers'; interface MappingField { name: string; @@ -30,10 +30,6 @@ export const formSetup = async (initTestBed: SetupFunc) => { testBed.find('backButton').simulate('click'); }; - const clickSubmitButton = () => { - testBed.find('submitButton').simulate('click'); - }; - const clickEditButtonAtField = (index: number) => { testBed.find('editFieldButton').at(index).simulate('click'); }; @@ -52,16 +48,100 @@ export const formSetup = async (initTestBed: SetupFunc) => { testBed.find('createFieldForm.cancelButton').simulate('click'); }; + // Step component templates actions + const componentTemplates = { + getComponentTemplatesInList() { + const { find } = testBed; + return find('componentTemplatesList.item.name').map((wrapper) => wrapper.text()); + }, + getComponentTemplatesSelected() { + const { find } = testBed; + return find('componentTemplatesSelection.item.name').map((wrapper) => wrapper.text()); + }, + showFilters() { + const { find, component } = testBed; + act(() => { + find('componentTemplates.filterButton').simulate('click'); + }); + component.update(); + }, + async selectFilter(filter: 'settings' | 'mappings' | 'aliases') { + const { find, component } = testBed; + const filters = ['settings', 'mappings', 'aliases']; + const index = filters.indexOf(filter); + + await act(async () => { + find('filterList.filterItem').at(index).simulate('click'); + }); + component.update(); + }, + async selectComponentAt(index: number) { + const { find, component } = testBed; + + await act(async () => { + find('componentTemplatesList.item.action-plusInCircle').at(index).simulate('click'); + }); + component.update(); + }, + async unSelectComponentAt(index: number) { + const { find, component } = testBed; + + await act(async () => { + find('componentTemplatesSelection.item.action-minusInCircle').at(index).simulate('click'); + }); + component.update(); + }, + }; + + // Step Mappings actions + const mappings = { + async addField(name: string, type: string) { + const { find, form, component } = testBed; + + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.mockComboBox').simulate('change', [ + { + label: type, + value: type, + }, + ]); + }); + + await act(async () => { + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); + }, + }; + + // Step Review actions + const review = { + async selectTab(tab: 'summary' | 'preview' | 'request') { + const tabs = ['summary', 'preview', 'request']; + + await act(async () => { + testBed.find('summaryTabContent').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); + }); + + testBed.component.update(); + }, + }; + const completeStepOne = async ({ name, indexPatterns, order, + priority, version, }: Partial = {}) => { - const { form, find, waitFor } = testBed; + const { component, form, find } = testBed; if (name) { - form.setInputValue('nameField.input', name); + act(() => { + form.setInputValue('nameField.input', name); + }); } if (indexPatterns) { @@ -70,94 +150,104 @@ export const formSetup = async (initTestBed: SetupFunc) => { value: pattern, })); - find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox - await nextTick(); + act(() => { + find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox + }); } - if (order) { - form.setInputValue('orderField.input', JSON.stringify(order)); - } + await act(async () => { + if (order) { + form.setInputValue('orderField.input', JSON.stringify(order)); + } - if (version) { - form.setInputValue('versionField.input', JSON.stringify(version)); - } + if (priority) { + form.setInputValue('priorityField.input', JSON.stringify(priority)); + } - clickNextButton(); - await waitFor('stepSettings'); + if (version) { + form.setInputValue('versionField.input', JSON.stringify(version)); + } + + clickNextButton(); + }); + + component.update(); }; - const completeStepTwo = async (settings?: string) => { - const { find, component, waitFor } = testBed; + const completeStepTwo = async (componentName?: string) => { + const { find, component } = testBed; + + if (componentName) { + // Find the index of the template in the list + const allComponents = find('componentTemplatesList.item.name').map((wrapper) => + wrapper.text() + ); + const index = allComponents.indexOf(componentName); + if (index < 0) { + throw new Error( + `Could not find component "${componentName}" in the list ${JSON.stringify(allComponents)}` + ); + } - if (settings) { - find('mockCodeEditor').simulate('change', { - jsonString: settings, - }); // Using mocked EuiCodeEditor - await nextTick(); - component.update(); + await componentTemplates.selectComponentAt(index); } - clickNextButton(); - await waitFor('stepMappings'); + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepThree = async (settings?: string) => { + const { find, component } = testBed; + + await act(async () => { + if (settings) { + find('mockCodeEditor').simulate('change', { + jsonString: settings, + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); }; - const completeStepThree = async (mappingFields?: MappingField[]) => { - const { waitFor } = testBed; + const completeStepFour = async (mappingFields?: MappingField[]) => { + const { component } = testBed; if (mappingFields) { for (const field of mappingFields) { const { name, type } = field; - await addMappingField(name, type); + await mappings.addField(name, type); } } - clickNextButton(); - await waitFor('stepAliases'); + await act(async () => { + clickNextButton(); + }); + + component.update(); }; - const completeStepFour = async (aliases?: string, waitForNextStep = true) => { - const { find, component, waitFor } = testBed; + const completeStepFive = async (aliases?: string) => { + const { find, component } = testBed; if (aliases) { - find('mockCodeEditor').simulate('change', { - jsonString: aliases, - }); // Using mocked EuiCodeEditor - await nextTick(); + await act(async () => { + find('mockCodeEditor').simulate('change', { + jsonString: aliases, + }); // Using mocked EuiCodeEditor + }); component.update(); } - clickNextButton(); - - if (waitForNextStep) { - await waitFor('summaryTab'); - } else { - component.update(); - } - }; - - const selectSummaryTab = (tab: 'summary' | 'request') => { - const tabs = ['summary', 'request']; - - testBed.find('summaryTabContent').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); - }; - - const addMappingField = async (name: string, type: string) => { - const { find, form, component } = testBed; - - form.setInputValue('nameParameterInput', name); - find('createFieldForm.mockComboBox').simulate('change', [ - { - label: type, - value: type, - }, - ]); - - await nextTick(50); - component.update(); - - find('createFieldForm.addButton').simulate('click'); + await act(async () => { + clickNextButton(); + }); - await nextTick(); component.update(); }; @@ -166,7 +256,6 @@ export const formSetup = async (initTestBed: SetupFunc) => { actions: { clickNextButton, clickBackButton, - clickSubmitButton, clickEditButtonAtField, clickEditFieldUpdateButton, deleteMappingsFieldAt, @@ -175,8 +264,10 @@ export const formSetup = async (initTestBed: SetupFunc) => { completeStepTwo, completeStepThree, completeStepFour, - selectSummaryTab, - addMappingField, + completeStepFive, + componentTemplates, + mappings, + review, }, }; }; @@ -187,6 +278,17 @@ export type TestSubjects = | 'backButton' | 'codeEditorContainer' | 'confirmModalConfirmButton' + | 'componentTemplates.filterButton' + | 'componentTemplates.emptySearchResult' + | 'filterList.filterItem' + | 'componentTemplatesList' + | 'componentTemplatesList.item.name' + | 'componentTemplatesList.item.action-plusInCircle' + | 'componentTemplatesSelection' + | 'componentTemplatesSelection.item.name' + | 'componentTemplatesSelection.item.action-minusInCircle' + | 'componentTemplatesSelection.emptyPrompt' + | 'componentTemplateSearchBox' | 'createFieldForm.addPropertyButton' | 'createFieldForm.addButton' | 'createFieldForm.addFieldButton' @@ -209,12 +311,15 @@ export type TestSubjects = | 'nextButton' | 'orderField' | 'orderField.input' + | 'priorityField.input' | 'pageTitle' + | 'previewTab' | 'removeFieldButton' | 'requestTab' | 'saveTemplateError' | 'settingsEditor' | 'systemTemplateEditCallout' + | 'stepComponents' | 'stepAliases' | 'stepMappings' | 'stepSettings' diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts index 93eb65aac0761..4b92235993e26 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts @@ -9,7 +9,7 @@ import { setup as componentTemplateDetailsSetup } from './component_template_det export { nextTick, getRandomString, findTestSubject } from '../../../../../../../../../test_utils'; -export { setupEnvironment } from './setup_environment'; +export { setupEnvironment, appDependencies } from './setup_environment'; export const pageHelpers = { componentTemplateList: { setup: componentTemplatesListSetup }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index 79e213229fc51..ac748e1b7dc2c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -24,7 +24,7 @@ import { API_BASE_PATH } from './constants'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); const { GlobalFlyoutProvider } = GlobalFlyout; -const appDependencies = { +export const appDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, apiBasePath: API_BASE_PATH, trackMetric: () => {}, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts new file mode 100644 index 0000000000000..ebd2cd9392568 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { appDependencies as componentTemplatesMockDependencies } from './client_integration/helpers'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx index b07279c57d2be..3c10ef2cc7edc 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx @@ -145,12 +145,13 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro /> } + data-test-subj="emptySearchResult" /> ); }; return ( -
+
@@ -162,6 +163,7 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro }} aria-label={i18nTexts.searchBoxPlaceholder} className="componentTemplates__searchBox" + data-test-subj="componentTemplateSearchBox" /> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx index 0c64c38c8963f..519f2482c9323 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx @@ -19,10 +19,10 @@ interface Props { export const ComponentTemplatesList = ({ components, listItemProps }: Props) => { return ( - <> +
{components.map((component) => ( ))} - +
); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx index ad75c8dcbcc54..40bb062f5ddf3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx @@ -48,6 +48,7 @@ export const ComponentTemplatesListItem = ({ className={classNames('componentTemplatesListItem', { 'componentTemplatesListItem--selected': isSelectedValue, })} + data-test-subj="item" > @@ -59,7 +60,7 @@ export const ComponentTemplatesListItem = ({
)} - + {/* {component.name} */} onViewDetail(component)}>{component.name} @@ -83,7 +84,7 @@ export const ComponentTemplatesListItem = ({ action.handler(component)} - data-test-subj="addPropertyButton" + data-test-subj={`action-${action.icon}`} aria-label={action.label} /> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx index ff871b8b79247..f2fd5048931de 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -160,6 +160,7 @@ export const ComponentTemplatesSelector = ({ // eslint-disable-next-line @typescript-eslint/naming-convention 'componentTemplatesSelector__selection--is-empty': !hasSelection, })} + data-test-subj="componentTemplatesSelection" > {hasSelection ? ( <> @@ -200,7 +201,7 @@ export const ComponentTemplatesSelector = ({
) : ( - +

0} numActiveFilters={activeFilters.length} - data-test-subj="viewButton" + data-test-subj="filterButton" > { - let testBed: MappingsEditorTestBed; - /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -34,6 +34,11 @@ describe('Mappings editor: shape datatype', () => { jest.useRealTimers(); }); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + test('initial view and default parameters values', async () => { const defaultMappings = { properties: { @@ -45,7 +50,10 @@ describe('Mappings editor: shape datatype', () => { const updatedMappings = { ...defaultMappings }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -53,7 +61,7 @@ describe('Mappings editor: shape datatype', () => { } = testBed; // Open the flyout to edit the field - startEditField('myField'); + await startEditField('myField'); // Save the field and close the flyout await updateFieldAndCloseFlyout(); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx index 66989baa2dc67..1832bedee0143 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx @@ -9,8 +9,6 @@ import { componentHelpers, MappingsEditorTestBed } from '../helpers'; import { getFieldConfig } from '../../../lib'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); // Parameters automatically added to the text datatype when saved (with the default values) export const defaultTextParameters = { @@ -24,14 +22,14 @@ export const defaultTextParameters = { store: false, }; -// FLAKY: https://github.com/elastic/kibana/issues/66669 -describe.skip('Mappings editor: text datatype', () => { - let testBed: MappingsEditorTestBed; - +describe('Mappings editor: text datatype', () => { /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -41,8 +39,9 @@ describe.skip('Mappings editor: text datatype', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('initial view and default parameters values', async () => { @@ -56,7 +55,10 @@ describe.skip('Mappings editor: text datatype', () => { const updatedMappings = { ...defaultMappings }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -64,7 +66,7 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; // Open the flyout to edit the field - startEditField('myField'); + await startEditField('myField'); // It should have searchable ("index" param) active by default const indexFieldConfig = getFieldConfig('index'); @@ -80,7 +82,7 @@ describe.skip('Mappings editor: text datatype', () => { ({ data } = await getMappingsEditorData(component)); expect(data).toEqual(updatedMappings); - }, 10000); + }); test('analyzer parameter: default values', async () => { const defaultMappings = { @@ -96,7 +98,10 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { component, @@ -113,8 +118,8 @@ describe.skip('Mappings editor: text datatype', () => { const fieldToEdit = 'myField'; // Start edit and immediately save to have all the default values - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); await updateFieldAndCloseFlyout(); expect(exists('mappingsEditorFieldEdit')).toBe(false); @@ -133,8 +138,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(data).toEqual(updatedMappings); // Re-open the edit panel - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); // When no analyzer is defined, defaults to "Index default" let indexAnalyzerValue = find('indexAnalyzer.select').props().value; @@ -158,9 +163,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(exists('searchAnalyzer')).toBe(false); // Uncheck the "Use same analyzer for search" checkbox and make sure the dedicated select appears - selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + selectCheckBox('useSameAnalyzerForSearchCheckBox.input', false); }); component.update(); @@ -169,7 +173,6 @@ describe.skip('Mappings editor: text datatype', () => { let searchAnalyzerValue = find('searchAnalyzer.select').props().value; expect(searchAnalyzerValue).toEqual('index_default'); - // Change the value of the 3 analyzers await act(async () => { // Change the value of the 3 analyzers setSelectValue('indexAnalyzer.select', 'standard', false); @@ -195,8 +198,8 @@ describe.skip('Mappings editor: text datatype', () => { expect(data).toEqual(updatedMappings); // Re-open the flyout and make sure the select have the correct updated value - startEditField('myField'); - showAdvancedSettings(); + await startEditField('myField'); + await showAdvancedSettings(); isUseSameAnalyzerForSearchChecked = getCheckboxValue('useSameAnalyzerForSearchCheckBox.input'); expect(isUseSameAnalyzerForSearchChecked).toBe(false); @@ -208,7 +211,7 @@ describe.skip('Mappings editor: text datatype', () => { expect(indexAnalyzerValue).toBe('standard'); expect(searchAnalyzerValue).toBe('simple'); expect(searchQuoteAnalyzerValue).toBe('whitespace'); - }, 50000); + }); test('analyzer parameter: custom analyzer (external plugin)', async () => { const defaultMappings = { @@ -234,7 +237,10 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { find, @@ -245,8 +251,8 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; const fieldToEdit = 'myField'; - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); expect(exists('indexAnalyzer-custom')).toBe(true); expect(exists('searchAnalyzer-custom')).toBe(true); @@ -301,7 +307,7 @@ describe.skip('Mappings editor: text datatype', () => { }; expect(data).toEqual(updatedMappings); - }, 100000); + }); test('analyzer parameter: custom analyzer (from index settings)', async () => { const indexSettings = { @@ -320,8 +326,6 @@ describe.skip('Mappings editor: text datatype', () => { const customAnalyzers = Object.keys(indexSettings.analysis.analyzer); const defaultMappings = { - _meta: {}, - _source: {}, properties: { myField: { type: 'text', @@ -340,11 +344,14 @@ describe.skip('Mappings editor: text datatype', () => { }, }; - testBed = setup({ - value: defaultMappings, - onChange: onChangeHandler, - indexSettings, + await act(async () => { + testBed = setup({ + value: defaultMappings, + onChange: onChangeHandler, + indexSettings, + }); }); + testBed.component.update(); const { component, @@ -354,8 +361,8 @@ describe.skip('Mappings editor: text datatype', () => { } = testBed; const fieldToEdit = 'myField'; - startEditField(fieldToEdit); - showAdvancedSettings(); + await startEditField(fieldToEdit); + await showAdvancedSettings(); // It should have 2 selects const indexAnalyzerSelects = find('indexAnalyzer.select'); @@ -395,5 +402,5 @@ describe.skip('Mappings editor: text datatype', () => { }; expect(data).toEqual(updatedMappings); - }, 50000); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx index c146c7704911f..9634817835101 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx @@ -8,10 +8,14 @@ import { act } from 'react-dom/test-utils'; import { componentHelpers, MappingsEditorTestBed } from './helpers'; import { defaultTextParameters, defaultShapeParameters } from './datatypes'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); describe('Mappings editor: edit field', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); let testBed: MappingsEditorTestBed; beforeAll(() => { @@ -22,8 +26,9 @@ describe('Mappings editor: edit field', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('should open a flyout with the correct field to edit', async () => { @@ -43,7 +48,11 @@ describe('Mappings editor: edit field', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + await testBed.actions.expandAllFieldsAndReturnMetadata(); const { @@ -51,7 +60,7 @@ describe('Mappings editor: edit field', () => { actions: { startEditField }, } = testBed; // Open the flyout to edit the field - startEditField('user.address.street'); + await startEditField('user.address.street'); // It should have the correct title expect(find('mappingsEditorFieldEdit.flyoutTitle').text()).toEqual(`Edit field 'street'`); @@ -72,7 +81,10 @@ describe('Mappings editor: edit field', () => { }, }; - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); const { find, @@ -83,19 +95,18 @@ describe('Mappings editor: edit field', () => { expect(exists('userNameField' as any)).toBe(true); // Open the flyout, change the field type and save it - startEditField('userName'); + await startEditField('userName'); // Change the field type - find('mappingsEditorFieldEdit.fieldType').simulate('change', [ - { label: 'Shape', value: defaultShapeParameters.type }, - ]); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find('mappingsEditorFieldEdit.fieldType').simulate('change', [ + { label: 'Shape', value: defaultShapeParameters.type }, + ]); }); await updateFieldAndCloseFlyout(); - const { data } = await getMappingsEditorData(component); + ({ data } = await getMappingsEditorData(component)); const updatedMappings = { ...defaultMappings, @@ -107,5 +118,5 @@ describe('Mappings editor: edit field', () => { }; expect(data).toEqual(updatedMappings); - }, 50000); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index a6558b28a1273..2a4af89c46559 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -149,22 +149,28 @@ const createActions = (testBed: TestBed) => { return { field: find(testSubject as TestSubjects), testSubject }; }; - const addField = (name: string, type: string) => { - form.setInputValue('nameParameterInput', name); - find('createFieldForm.fieldType').simulate('change', [ - { - label: type, - value: type, - }, - ]); - find('createFieldForm.addButton').simulate('click'); + const addField = async (name: string, type: string) => { + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.fieldType').simulate('change', [ + { + label: type, + value: type, + }, + ]); + }); + + await act(async () => { + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); }; - const startEditField = (path: string) => { + const startEditField = async (path: string) => { const { testSubject } = getFieldAt(path); - find(`${testSubject}.editFieldButton` as TestSubjects).simulate('click'); - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find(`${testSubject}.editFieldButton` as TestSubjects).simulate('click'); }); component.update(); }; @@ -174,34 +180,33 @@ const createActions = (testBed: TestBed) => { find('mappingsEditorFieldEdit.editFieldUpdateButton').simulate('click'); }); component.update(); - - act(() => { - jest.advanceTimersByTime(1000); - }); }; - const showAdvancedSettings = () => { + const showAdvancedSettings = async () => { if (find('mappingsEditorFieldEdit.advancedSettings').props().style.display === 'block') { // Already opened, nothing else to do return; } - find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); - - act(() => { - jest.advanceTimersByTime(1000); + await act(async () => { + find('mappingsEditorFieldEdit.toggleAdvancedSetting').simulate('click'); }); + component.update(); }; - const selectTab = (tab: 'fields' | 'templates' | 'advanced') => { + const selectTab = async (tab: 'fields' | 'templates' | 'advanced') => { const index = ['fields', 'templates', 'advanced'].indexOf(tab); const tabElement = find('formTab').at(index); if (tabElement.length === 0) { throw new Error(`Tab not found: "${tab}"`); } - tabElement.simulate('click'); + + await act(async () => { + tabElement.simulate('click'); + }); + component.update(); }; const updateJsonEditor = (testSubject: TestSubjects, value: object) => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 0743211a2b7bf..68933ddc9a935 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -8,15 +8,15 @@ import { act } from 'react-dom/test-utils'; import { componentHelpers, MappingsEditorTestBed } from './helpers'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; -const onChangeHandler = jest.fn(); -const getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); -// FLAKY: https://github.com/elastic/kibana/issues/66457 -describe.skip('Mappings editor: core', () => { +describe('Mappings editor: core', () => { /** * Variable to store the mappings data forwarded to the consumer component */ let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; beforeAll(() => { jest.useFakeTimers(); @@ -26,8 +26,9 @@ describe.skip('Mappings editor: core', () => { jest.useRealTimers(); }); - afterEach(() => { - onChangeHandler.mockReset(); + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); }); test('default behaviour', async () => { @@ -42,11 +43,14 @@ describe.skip('Mappings editor: core', () => { }, }; - const { component } = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + + const { component } = testBed; + component.update(); const expectedMappings = { - _meta: {}, // Was not defined so an empty object is returned - _source: {}, // Was not defined so an empty object is returned ...defaultMappings, properties: { user: { @@ -78,8 +82,13 @@ describe.skip('Mappings editor: core', () => { }, }, }; - const testBed = setup({ onChange: onChangeHandler, value }); - const { exists } = testBed; + + await act(async () => { + testBed = setup({ onChange: onChangeHandler, value }); + }); + + const { component, exists } = testBed; + component.update(); expect(exists('mappingsEditor')).toBe(true); expect(exists('mappingTypesDetectedCallout')).toBe(true); @@ -94,8 +103,12 @@ describe.skip('Mappings editor: core', () => { }, }, }; - const testBed = setup({ onChange: onChangeHandler, value }); - const { exists } = testBed; + await act(async () => { + testBed = setup({ onChange: onChangeHandler, value }); + }); + + const { component, exists } = testBed; + component.update(); expect(exists('mappingsEditor')).toBe(true); expect(exists('mappingTypesDetectedCallout')).toBe(false); @@ -108,10 +121,12 @@ describe.skip('Mappings editor: core', () => { properties: {}, dynamic_templates: [{ before: 'foo' }], }; - let testBed: MappingsEditorTestBed; beforeEach(async () => { - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); }); test('should keep the changes when switching tabs', async () => { @@ -129,10 +144,7 @@ describe.skip('Mappings editor: core', () => { expect(find('fieldsListItem').length).toEqual(0); // Check that we start with an empty list const newField = { name: 'John', type: 'text' }; - await act(async () => { - addField(newField.name, newField.type); - }); - component.update(); + await addField(newField.name, newField.type); expect(find('fieldsListItem').length).toEqual(1); @@ -142,10 +154,7 @@ describe.skip('Mappings editor: core', () => { // ------------------------------------- // Navigate to dynamic templates tab // ------------------------------------- - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); let templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); expect(templatesValue).toEqual(defaultMappings.dynamic_templates); @@ -163,10 +172,7 @@ describe.skip('Mappings editor: core', () => { // ------------------------------------------------------ // Switch to advanced settings tab and make some changes // ------------------------------------------------------ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); let isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -194,10 +200,7 @@ describe.skip('Mappings editor: core', () => { // ---------------------------------------------------------------------------- // Go back to dynamic templates tab and make sure our changes are still there // ---------------------------------------------------------------------------- - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); expect(templatesValue).toEqual(updatedValueTemplates); @@ -205,18 +208,13 @@ describe.skip('Mappings editor: core', () => { // ----------------------------------------------------------- // Go back to fields and make sure our created field is there // ----------------------------------------------------------- - await act(async () => { - selectTab('fields'); - }); - component.update(); + await selectTab('fields'); + field = find('fieldsListItem').at(0); expect(find('fieldName', field).text()).toEqual(newField.name); // Go back to advanced settings tab make sure dynamic mappings is disabled - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -231,46 +229,47 @@ describe.skip('Mappings editor: core', () => { /** * Note: the "indexSettings" prop will be tested along with the "analyzer" parameter on a text datatype field, * as it is the only place where it is consumed by the mappings editor. - * - * The test that covers it is text_datatype.test.tsx: "analyzer parameter: custom analyzer (from index settings)" + * The test that covers it is in the "text_datatype.test.tsx": "analyzer parameter: custom analyzer (from index settings)" */ - const defaultMappings: any = { - dynamic: true, - numeric_detection: false, - date_detection: true, - properties: { - title: { type: 'text' }, - address: { - type: 'object', - properties: { - street: { type: 'text' }, - city: { type: 'text' }, + let defaultMappings: any; + + beforeEach(async () => { + defaultMappings = { + dynamic: true, + numeric_detection: false, + date_detection: true, + properties: { + title: { type: 'text' }, + address: { + type: 'object', + properties: { + street: { type: 'text' }, + city: { type: 'text' }, + }, }, }, - }, - dynamic_templates: [{ initial: 'value' }], - _source: { - enabled: true, - includes: ['field1', 'field2'], - excludes: ['field3'], - }, - _meta: { - some: 'metaData', - }, - _routing: { - required: false, - }, - }; - - let testBed: MappingsEditorTestBed; + dynamic_templates: [{ initial: 'value' }], + _source: { + enabled: true, + includes: ['field1', 'field2'], + excludes: ['field3'], + }, + _meta: { + some: 'metaData', + }, + _routing: { + required: false, + }, + }; - beforeEach(async () => { - testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); }); test('props.value => should prepopulate the editor data', async () => { const { - component, actions: { selectTab, getJsonEditorValue, getComboBoxValue, getToggleValue }, find, } = testBed; @@ -285,10 +284,7 @@ describe.skip('Mappings editor: core', () => { /** * Dynamic templates */ - await act(async () => { - selectTab('templates'); - }); - component.update(); + await selectTab('templates'); // Test that dynamic templates JSON is rendered in the templates editor const templatesValue = getJsonEditorValue('dynamicTemplatesEditor'); @@ -297,10 +293,7 @@ describe.skip('Mappings editor: core', () => { /** * Advanced settings */ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); const isDynamicMappingsEnabled = getToggleValue( 'advancedConfiguration.dynamicMappingsToggle.input' @@ -339,7 +332,14 @@ describe.skip('Mappings editor: core', () => { /** * Mapped fields */ + await act(async () => { + find('addFieldButton').simulate('click'); + }); + component.update(); + const newField = { name: 'someNewField', type: 'text' }; + await addField(newField.name, newField.type); + updatedMappings = { ...updatedMappings, properties: { @@ -348,26 +348,14 @@ describe.skip('Mappings editor: core', () => { }, }; - await act(async () => { - find('addFieldButton').simulate('click'); - }); - component.update(); - - await act(async () => { - addField(newField.name, newField.type); - }); - component.update(); - ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); /** * Dynamic templates */ - await act(async () => { - await selectTab('templates'); - }); - component.update(); + await selectTab('templates'); const updatedTemplatesValue = [{ someTemplateProp: 'updated' }]; updatedMappings = { @@ -385,10 +373,7 @@ describe.skip('Mappings editor: core', () => { /** * Advanced settings */ - await act(async () => { - selectTab('advanced'); - }); - component.update(); + await selectTab('advanced'); // Disbable dynamic mappings await act(async () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx index dc52a362008c6..1457c4583aa0e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -71,9 +71,8 @@ export const AnalyzerParameter = ({ allowsIndexDefaultOption = true, 'data-test-subj': dataTestSubj, }: Props) => { - const indexSettings = useIndexSettings(); + const { value: indexSettings } = useIndexSettings(); const customAnalyzers = getCustomAnalyzers(indexSettings); - const analyzerOptions = allowsIndexDefaultOption ? ANALYZER_OPTIONS : ANALYZER_OPTIONS_WITHOUT_DEFAULT; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index 17d3ea0909bfb..c966df82fb507 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -49,16 +49,17 @@ export const AnalyzerParameterSelects = ({ 'data-test-subj': dataTestSubj, }: Props) => { const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } }); + const { subscribe } = form; useEffect(() => { - const subscription = form.subscribe((updateData) => { + const subscription = subscribe((updateData) => { const formData = updateData.data.raw; const value = formData.sub ? formData.sub : formData.main; onChange(value); }); return subscription.unsubscribe; - }, [form, onChange]); + }, [subscribe, onChange]); const getSubOptionsMeta = useCallback( (mainValue: string) => diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx index 411193f10b24a..bd84c3a905ec8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx @@ -3,23 +3,32 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { IndexSettings } from './types'; -const IndexSettingsContext = createContext(undefined); +const IndexSettingsContext = createContext< + { value: IndexSettings; update: (value: IndexSettings) => void } | undefined +>(undefined); interface Props { - indexSettings: IndexSettings | undefined; children: React.ReactNode; } -export const IndexSettingsProvider = ({ indexSettings = {}, children }: Props) => ( - {children} -); +export const IndexSettingsProvider = ({ children }: Props) => { + const [state, setState] = useState({}); + + return ( + + {children} + + ); +}; export const useIndexSettings = () => { const ctx = useContext(IndexSettingsContext); - - return ctx === undefined ? {} : ctx; + if (ctx === undefined) { + throw new Error('useIndexSettings must be used within a '); + } + return ctx; }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 963603d241ee4..5882496f6dc0a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -25,7 +25,7 @@ import { import { extractMappingsDefinition } from './lib'; import { useMappingsState } from './mappings_state_context'; import { useMappingsStateListener } from './use_state_listener'; -import { IndexSettingsProvider } from './index_settings_context'; +import { useIndexSettings } from './index_settings_context'; type TabName = 'fields' | 'advanced' | 'templates'; @@ -98,6 +98,12 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr */ useMappingsStateListener({ onChange, value: parsedDefaultValue, mappingsType }); + // Update the Index settings context so it is available in the Global flyout + const { update: updateIndexSettings } = useIndexSettings(); + if (indexSettings !== undefined) { + updateIndexSettings(indexSettings); + } + const state = useMappingsState(); const [selectedTab, selectTab] = useState('fields'); @@ -145,43 +151,41 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr {multipleMappingsDeclared ? ( ) : ( - -

- - changeTab('fields')} - isSelected={selectedTab === 'fields'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { - defaultMessage: 'Mapped fields', - })} - - changeTab('templates')} - isSelected={selectedTab === 'templates'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { - defaultMessage: 'Dynamic templates', - })} - - changeTab('advanced')} - isSelected={selectedTab === 'advanced'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { - defaultMessage: 'Advanced options', - })} - - - - - - {tabToContentMap[selectedTab]} -
- +
+ + changeTab('fields')} + isSelected={selectedTab === 'fields'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { + defaultMessage: 'Mapped fields', + })} + + changeTab('templates')} + isSelected={selectedTab === 'templates'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { + defaultMessage: 'Dynamic templates', + })} + + changeTab('advanced')} + isSelected={selectedTab === 'advanced'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { + defaultMessage: 'Advanced options', + })} + + + + + + {tabToContentMap[selectedTab]} +
)}
); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx index 596b49cc89ee8..8e30d07c2262f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx @@ -6,7 +6,12 @@ import React from 'react'; import { StateProvider } from './mappings_state_context'; +import { IndexSettingsProvider } from './index_settings_context'; export const MappingsEditorProvider: React.FC = ({ children }) => { - return {children}; + return ( + + {children} + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 2a8368c666859..e7efd6f28343b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -175,10 +175,18 @@ export const reducer = (state: State, action: Action): State => { fields: action.value.fields, configuration: { ...state.configuration, + data: { + raw: action.value.configuration, + format: () => action.value.configuration, + }, defaultValue: action.value.configuration, }, templates: { ...state.templates, + data: { + raw: action.value.templates, + format: () => action.value.templates, + }, defaultValue: action.value.templates, }, documentFields: { diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 3b9de2b3409b6..ac6e8b7879a26 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -11,6 +11,42 @@ const objHasProperties = (obj?: Record): boolean => { return obj === undefined || Object.keys(obj).length === 0 ? false : true; }; +export const getComposableTemplate = ({ + name = getRandomString(), + version = getRandomNumber(), + priority = getRandomNumber(), + indexPatterns = [], + template: { settings, aliases, mappings } = {}, + hasDatastream = false, + isLegacy = false, + type = 'default', +}: Partial< + TemplateDeserialized & { + isLegacy?: boolean; + type?: TemplateType; + hasDatastream: boolean; + } +> = {}): TemplateDeserialized => { + const indexTemplate = { + name, + version, + priority, + indexPatterns, + template: { + aliases, + mappings, + settings, + }, + _kbnMeta: { + type, + hasDatastream, + isLegacy, + }, + }; + + return indexTemplate; +}; + export const getTemplate = ({ name = getRandomString(), version = getRandomNumber(), diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 551a0f7d60164..3e065142ea101 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -21,7 +21,8 @@ export const EPM_API_ROUTES = { LIST_PATTERN: EPM_PACKAGES_MANY, LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, - INSTALL_PATTERN: EPM_PACKAGES_ONE, + INSTALL_FROM_REGISTRY_PATTERN: EPM_PACKAGES_ONE, + INSTALL_BY_UPLOAD_PATTERN: EPM_PACKAGES_MANY, DELETE_PATTERN: EPM_PACKAGES_ONE, FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 1d802739a1b86..b7521f95b4f83 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -40,7 +40,10 @@ export const epmRouteService = { }, getInstallPath: (pkgkey: string) => { - return EPM_API_ROUTES.INSTALL_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash + return EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN.replace('{pkgkey}', pkgkey).replace( + /\/$/, + '' + ); // trim trailing slash }, getRemovePath: (pkgkey: string) => { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 5fb718f91b876..54e767fee4b22 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,6 +71,10 @@ export interface InstallPackageResponse { response: AssetReference[]; } +export interface MessageResponse { + response: string; +} + export interface DeletePackageRequest { params: { pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 385e256933c12..c40e0e4ac5c0b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -9,6 +9,7 @@ import { appContextService } from '../../services'; import { GetInfoResponse, InstallPackageResponse, + MessageResponse, DeletePackageResponse, GetCategoriesResponse, GetPackagesResponse, @@ -19,7 +20,8 @@ import { GetPackagesRequestSchema, GetFileRequestSchema, GetInfoRequestSchema, - InstallPackageRequestSchema, + InstallPackageFromRegistryRequestSchema, + InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, } from '../../types'; import { @@ -129,10 +131,10 @@ export const getInfoHandler: RequestHandler, +export const installPackageFromRegistryHandler: RequestHandler< + TypeOf, undefined, - TypeOf + TypeOf > = async (context, request, response) => { const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; @@ -183,6 +185,17 @@ export const installPackageHandler: RequestHandler< } }; +export const installPackageByUploadHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const body: MessageResponse = { + response: 'package upload was received ok, but not installed (not implemented yet)', + }; + return response.ok({ body }); +}; + export const deletePackageHandler: RequestHandler> = async (context, request, response) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index b524a7b33923e..9048652f0e8a9 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -11,7 +11,8 @@ import { getLimitedListHandler, getFileHandler, getInfoHandler, - installPackageHandler, + installPackageFromRegistryHandler, + installPackageByUploadHandler, deletePackageHandler, } from './handlers'; import { @@ -19,10 +20,13 @@ import { GetPackagesRequestSchema, GetFileRequestSchema, GetInfoRequestSchema, - InstallPackageRequestSchema, + InstallPackageFromRegistryRequestSchema, + InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, } from '../../types'; +const MAX_FILE_SIZE_BYTES = 104857600; // 100MB + export const registerRoutes = (router: IRouter) => { router.get( { @@ -71,11 +75,27 @@ export const registerRoutes = (router: IRouter) => { router.post( { - path: EPM_API_ROUTES.INSTALL_PATTERN, - validate: InstallPackageRequestSchema, + path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN, + validate: InstallPackageFromRegistryRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - installPackageHandler + installPackageFromRegistryHandler + ); + + router.post( + { + path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, + validate: InstallPackageByUploadRequestSchema, + options: { + tags: [`access:${PLUGIN_ID}-all`], + body: { + accepts: ['application/gzip', 'application/zip'], + parse: false, + maxBytes: MAX_FILE_SIZE_BYTES, + }, + }, + }, + installPackageByUploadHandler ); router.delete( diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 191014606f220..d7a801feec34f 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -32,7 +32,7 @@ export const GetInfoRequestSchema = { }), }; -export const InstallPackageRequestSchema = { +export const InstallPackageFromRegistryRequestSchema = { params: schema.object({ pkgkey: schema.string(), }), @@ -43,6 +43,10 @@ export const InstallPackageRequestSchema = { ), }; +export const InstallPackageByUploadRequestSchema = { + body: schema.buffer(), +}; + export const DeletePackageRequestSchema = { params: schema.object({ pkgkey: schema.string(), diff --git a/x-pack/plugins/lens/public/app_plugin/_app.scss b/x-pack/plugins/lens/public/app_plugin/_app.scss index 4ad8dd360bac6..8416577a60421 100644 --- a/x-pack/plugins/lens/public/app_plugin/_app.scss +++ b/x-pack/plugins/lens/public/app_plugin/_app.scss @@ -26,3 +26,17 @@ flex-direction: column; flex-grow: 1; } + +.lensChartIcon__subdued { + fill: $euiTextSubduedColor; + + // Not great, but the easiest way to fix the gray fill when stuck in a button with a fill + // Like when selected in a button group + .euiButton--fill & { + fill: currentColor; + } +} + +.lensChartIcon__accent { + fill: $euiColorVis0; +} diff --git a/x-pack/plugins/lens/public/assets/chart_area.svg b/x-pack/plugins/lens/public/assets/chart_area.svg deleted file mode 100644 index d291a084028db..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_area.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_area.tsx b/x-pack/plugins/lens/public/assets/chart_area.tsx new file mode 100644 index 0000000000000..ae817e9794dc5 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_area.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartArea = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_area_percentage.tsx b/x-pack/plugins/lens/public/assets/chart_area_percentage.tsx new file mode 100644 index 0000000000000..45c208d5d634b --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_area_percentage.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartAreaPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_area_stacked.svg b/x-pack/plugins/lens/public/assets/chart_area_stacked.svg deleted file mode 100644 index 6ae48bf6a640b..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_area_stacked.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_area_stacked.tsx b/x-pack/plugins/lens/public/assets/chart_area_stacked.tsx new file mode 100644 index 0000000000000..0320ad7e9afa5 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_area_stacked.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartAreaStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar.svg b/x-pack/plugins/lens/public/assets/chart_bar.svg deleted file mode 100644 index 44553960a5cce..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_bar.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_bar.tsx b/x-pack/plugins/lens/public/assets/chart_bar.tsx new file mode 100644 index 0000000000000..9408f77bd4237 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBar = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg deleted file mode 100644 index e0d9dc8385971..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_bar_horizontal.tsx b/x-pack/plugins/lens/public/assets/chart_bar_horizontal.tsx new file mode 100644 index 0000000000000..7ec48b107e2fb --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_horizontal.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontal = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_horizontal_percentage.tsx b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_percentage.tsx new file mode 100644 index 0000000000000..6ce09265d4894 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_percentage.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontalPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg deleted file mode 100644 index 602a06e696ecd..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.tsx b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.tsx new file mode 100644 index 0000000000000..c862121fd04f2 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontalStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_percentage.tsx b/x-pack/plugins/lens/public/assets/chart_bar_percentage.tsx new file mode 100644 index 0000000000000..b7d6a0ed604af --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_percentage.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_stacked.svg deleted file mode 100644 index a954cce83873d..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_bar_stacked.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_bar_stacked.tsx b/x-pack/plugins/lens/public/assets/chart_bar_stacked.tsx new file mode 100644 index 0000000000000..edf8e675178f0 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_stacked.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_datatable.svg b/x-pack/plugins/lens/public/assets/chart_datatable.svg deleted file mode 100644 index aba1f104264cb..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_datatable.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_datatable.tsx b/x-pack/plugins/lens/public/assets/chart_datatable.tsx new file mode 100644 index 0000000000000..48cc844ea2805 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_datatable.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartDatatable = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_donut.svg b/x-pack/plugins/lens/public/assets/chart_donut.svg deleted file mode 100644 index 5e0d8b7ea83bf..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_donut.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_donut.tsx b/x-pack/plugins/lens/public/assets/chart_donut.tsx new file mode 100644 index 0000000000000..9482161de9d9e --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_donut.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartDonut = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_line.svg b/x-pack/plugins/lens/public/assets/chart_line.svg deleted file mode 100644 index 412c9f88f652b..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_line.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_line.tsx b/x-pack/plugins/lens/public/assets/chart_line.tsx new file mode 100644 index 0000000000000..5b57e1fe28c16 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_line.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartLine = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_metric.svg b/x-pack/plugins/lens/public/assets/chart_metric.svg deleted file mode 100644 index 84f0dc181587b..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_metric.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_metric.tsx b/x-pack/plugins/lens/public/assets/chart_metric.tsx new file mode 100644 index 0000000000000..9faa4d6584258 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_metric.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartMetric = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_mixed_xy.svg b/x-pack/plugins/lens/public/assets/chart_mixed_xy.svg deleted file mode 100644 index 943d5a08bcc0b..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_mixed_xy.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_mixed_xy.tsx b/x-pack/plugins/lens/public/assets/chart_mixed_xy.tsx new file mode 100644 index 0000000000000..08eac8eb1605d --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_mixed_xy.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartMixedXy = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_pie.svg b/x-pack/plugins/lens/public/assets/chart_pie.svg deleted file mode 100644 index 22faaf5d97661..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_pie.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_pie.tsx b/x-pack/plugins/lens/public/assets/chart_pie.tsx new file mode 100644 index 0000000000000..cc26df4419caf --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_pie.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartPie = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_treemap.svg b/x-pack/plugins/lens/public/assets/chart_treemap.svg deleted file mode 100644 index b0ee04d02b2a6..0000000000000 --- a/x-pack/plugins/lens/public/assets/chart_treemap.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/x-pack/plugins/lens/public/assets/chart_treemap.tsx b/x-pack/plugins/lens/public/assets/chart_treemap.tsx new file mode 100644 index 0000000000000..57205e94137a8 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_treemap.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartTreemap = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index b9bdea5522f32..eb00cf93ccd34 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -15,6 +15,7 @@ import { IFieldFormat } from '../../../../../src/plugins/data/public'; import { IAggType } from 'src/plugins/data/public'; const onClickValue = jest.fn(); import { EmptyPlaceholder } from '../shared_components'; +import { LensIconChartDatatable } from '../assets/chart_datatable'; function sampleArgs() { const data: LensMultiTable = { @@ -219,7 +220,7 @@ describe('datatable_expression', () => { )} /> ); - expect(component.find(EmptyPlaceholder).prop('icon')).toEqual('visTable'); + expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDatatable); }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 87ac2d1710b19..dac3b23b98e3b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -23,6 +23,8 @@ import { import { VisualizationContainer } from '../visualization_container'; import { EmptyPlaceholder } from '../shared_components'; import { desanitizeFilterContext } from '../utils'; +import { LensIconChartDatatable } from '../assets/chart_datatable'; + export interface DatatableColumns { columnIds: string[]; } @@ -199,7 +201,7 @@ export function DatatableComponent(props: DatatableRenderProps) { )); if (isEmpty) { - return ; + return ; } return ( diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 836ffcb15cfa1..5b462f44b3dd5 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -7,7 +7,7 @@ import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; -import chartTableSVG from '../assets/chart_datatable.svg'; +import { LensIconChartDatatable } from '../assets/chart_datatable'; export interface LayerState { layerId: string; @@ -31,8 +31,7 @@ export const datatableVisualization: Visualization visualizationTypes: [ { id: 'lnsDatatable', - icon: 'visTable', - largeIcon: chartTableSVG, + icon: LensIconChartDatatable, label: i18n.translate('xpack.lens.datatable.label', { defaultMessage: 'Data table', }), @@ -55,7 +54,7 @@ export const datatableVisualization: Visualization getDescription() { return { - icon: chartTableSVG, + icon: LensIconChartDatatable, label: i18n.translate('xpack.lens.datatable.label', { defaultMessage: 'Data table', }), @@ -121,7 +120,7 @@ export const datatableVisualization: Visualization }, ], }, - previewIcon: chartTableSVG, + previewIcon: LensIconChartDatatable, // tables are hidden from suggestion bar, but used for drag & drop and chart switching hide: true, }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss index 1d088e2ec86f9..9d018076dc320 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss @@ -42,9 +42,8 @@ justify-content: center; padding: $euiSizeS; - // Targeting img as this won't target normal EuiIcon's only the custom svgs's - > img { - @include size($euiSize * 4); + &:not(:only-child) { + height: calc(100% - #{$euiSizeL}); } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index ab53ff983ca26..0329c856f97f2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -6,6 +6,14 @@ max-width: calc(100% - #{$euiSize * 3.625}); } +.lnsLayerPanel__settingsFlexItem:empty + .lnsLayerPanel__sourceFlexItem { + max-width: calc(100% - #{$euiSizeS}); +} + +.lnsLayerPanel__settingsFlexItem:empty { + margin: 0; +} + .lnsLayerPanel__row { background: $euiColorLightestShade; padding: $euiSizeS; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index b45dd13bfa4fd..e9de2f935f0f3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -100,7 +100,7 @@ export function LayerPanel( - + { getSuggestionsMock.mockReturnValue([ { datasourceState: {}, - previewIcon: chartTableSVG, + previewIcon: LensIconChartDatatable, score: 0.5, visualizationState: suggestion1State, visualizationId: 'vis', @@ -288,6 +288,6 @@ describe('suggestion_panel', () => { const wrapper = mount(); expect(wrapper.find(EuiIcon)).toHaveLength(1); - expect(wrapper.find(EuiIcon).prop('type')).toEqual(chartTableSVG); + expect(wrapper.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss index 8a44d59ff1c0d..f84191e1bfb1a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss @@ -8,6 +8,7 @@ .lnsChartSwitch__summaryIcon { margin-right: $euiSizeS; transform: translateY(-1px); + color: $euiTextSubduedColor; } // Targeting img as this won't target normal EuiIcon's only the custom svgs's diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 5640c52ac4325..82983862e7c03 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -66,7 +66,7 @@ function VisualizationSummary(props: Props) { return ( <> {description.icon && ( - + )} {description.label} @@ -181,7 +181,7 @@ export function ChartSwitch(props: Props) { v.visualizationTypes.map((t) => ({ visualizationId: v.id, ...t, - icon: t.largeIcon || t.icon, + icon: t.icon, })) ) ).map((visualizationType) => ({ diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts index ecb1f07214ac6..2a659e5fe10c4 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts @@ -100,7 +100,7 @@ describe('metric_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestion).toMatchInlineSnapshot(` Object { - "previewIcon": "test-file-stub", + "previewIcon": [Function], "score": 0.1, "state": Object { "accessor": "bytes", diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts index 0caac7dd0d092..c95467ab04e11 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts @@ -6,7 +6,7 @@ import { SuggestionRequest, VisualizationSuggestion, TableSuggestion } from '../types'; import { State } from './types'; -import chartMetricSVG from '../assets/chart_metric.svg'; +import { LensIconChartMetric } from '../assets/chart_metric'; /** * Generate suggestions for the metric chart. @@ -44,7 +44,7 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion { return { title, score: 0.1, - previewIcon: chartMetricSVG, + previewIcon: LensIconChartMetric, state: { layerId: table.layerId, accessor: col.columnId, diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 5f1ce5334dd36..72c07bed1acb2 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; +import { LensIconChartMetric } from '../assets/chart_metric'; import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types'; import { State } from './types'; -import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, @@ -45,8 +45,7 @@ export const metricVisualization: Visualization = { visualizationTypes: [ { id: 'lnsMetric', - icon: 'visMetric', - largeIcon: chartMetricSVG, + icon: LensIconChartMetric, label: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric', }), @@ -70,7 +69,7 @@ export const metricVisualization: Visualization = { getDescription() { return { - icon: chartMetricSVG, + icon: LensIconChartMetric, label: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric', }), diff --git a/x-pack/plugins/lens/public/pie_visualization/constants.ts b/x-pack/plugins/lens/public/pie_visualization/constants.ts index 10672f91a81c7..ab5e64ea84029 100644 --- a/x-pack/plugins/lens/public/pie_visualization/constants.ts +++ b/x-pack/plugins/lens/public/pie_visualization/constants.ts @@ -5,25 +5,25 @@ */ import { i18n } from '@kbn/i18n'; -import chartDonutSVG from '../assets/chart_donut.svg'; -import chartPieSVG from '../assets/chart_pie.svg'; -import chartTreemapSVG from '../assets/chart_treemap.svg'; +import { LensIconChartDonut } from '../assets/chart_donut'; +import { LensIconChartPie } from '../assets/chart_pie'; +import { LensIconChartTreemap } from '../assets/chart_treemap'; export const CHART_NAMES = { donut: { - icon: chartDonutSVG, + icon: LensIconChartDonut, label: i18n.translate('xpack.lens.pie.donutLabel', { defaultMessage: 'Donut', }), }, pie: { - icon: chartPieSVG, + icon: LensIconChartPie, label: i18n.translate('xpack.lens.pie.pielabel', { defaultMessage: 'Pie', }), }, treemap: { - icon: chartTreemapSVG, + icon: LensIconChartTreemap, label: i18n.translate('xpack.lens.pie.treemaplabel', { defaultMessage: 'Treemap', }), @@ -33,4 +33,4 @@ export const CHART_NAMES = { export const MAX_PIE_BUCKETS = 3; export const MAX_TREEMAP_BUCKETS = 2; -export const DEFAULT_PERCENT_DECIMALS = 3; +export const DEFAULT_PERCENT_DECIMALS = 2; diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx index 855bacd4f794c..dd1b36e00ebb9 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx @@ -37,17 +37,17 @@ export const pieVisualization: Visualization = { visualizationTypes: [ { id: 'donut', - largeIcon: CHART_NAMES.donut.icon, + icon: CHART_NAMES.donut.icon, label: CHART_NAMES.donut.label, }, { id: 'pie', - largeIcon: CHART_NAMES.pie.icon, + icon: CHART_NAMES.pie.icon, label: CHART_NAMES.pie.label, }, { id: 'treemap', - largeIcon: CHART_NAMES.treemap.icon, + icon: CHART_NAMES.treemap.icon, label: CHART_NAMES.treemap.label, }, ], diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 38ef44a2fef18..ac952e307758b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -12,6 +12,7 @@ import { PieComponent } from './render_function'; import { PieExpressionArgs } from './types'; import { EmptyPlaceholder } from '../shared_components'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { LensIconChartDonut } from '../assets/chart_donut'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -189,7 +190,7 @@ describe('PieVisualization component', () => { const component = shallow( ); - expect(component.find(EmptyPlaceholder).prop('icon')).toEqual('visPie'); + expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); }); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 4e813494d7d32..d97ab146e000d 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -33,6 +33,7 @@ import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; import { desanitizeFilterContext } from '../utils'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { LensIconChartDonut } from '../assets/chart_donut'; const EMPTY_SLICE = Symbol('empty_slice'); @@ -186,7 +187,7 @@ export function PieComponent( const percentFormatter = props.formatFactory({ id: 'percent', params: { - pattern: `0,0.${'0'.repeat(percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}%`, + pattern: `0,0.[${'0'.repeat(percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`, }, }); @@ -210,7 +211,7 @@ export function PieComponent( ); if (isEmpty) { - return ; + return ; } if (hasNegative) { diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 501a2de24d9ad..9ee37fdf53a92 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -188,10 +188,10 @@ export function PieToolbar(props: VisualizationToolbarProps yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; - const formatter: SerializedFieldFormat = table.columns.find( - (column) => column.id === accessor - )?.formatHint || { id: 'number' }; + let formatter: SerializedFieldFormat = table.columns.find((column) => column.id === accessor) + ?.formatHint || { id: 'number' }; + if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { + formatter = { + id: 'percent', + params: { + pattern: '0.[00]%', + }, + }; + } series[mode].push({ layer: layer.layerId, accessor, diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index 2ddb9418abad9..41d18e5199e4c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -8,7 +8,11 @@ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { SeriesType, visualizationTypes, LayerConfig, YConfig } from './types'; export function isHorizontalSeries(seriesType: SeriesType) { - return seriesType === 'bar_horizontal' || seriesType === 'bar_horizontal_stacked'; + return ( + seriesType === 'bar_horizontal' || + seriesType === 'bar_horizontal_stacked' || + seriesType === 'bar_horizontal_percentage_stacked' + ); } export function isHorizontalChart(layers: Array<{ seriesType: SeriesType }>) { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index f579085646f6f..825281d6d88c2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -100,25 +100,26 @@ describe('#toExpression', () => { expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true); }); - it('should not generate an expression when missing x', () => { - expect( - xyVisualization.toExpression( - { - legend: { position: Position.Bottom, isVisible: true }, - preferredSeriesType: 'bar', - layers: [ - { - layerId: 'first', - seriesType: 'area', - splitAccessor: undefined, - xAccessor: undefined, - accessors: ['a'], - }, - ], - }, - frame.datasourceLayers - ) - ).toBeNull(); + it('should generate an expression without x accessor', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + frame.datasourceLayers + ) as Ast; + expect((expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.xAccessor).toEqual( + [] + ); }); it('should not generate an expression when missing y', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index cd32d4f94c3e5..f64624776186d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -84,7 +84,7 @@ export const buildExpression = ( datasourceLayers?: Record ): Ast | null => { const validLayers = state.layers.filter((layer): layer is ValidLayer => - Boolean(layer.xAccessor && layer.accessors.length) + Boolean(layer.accessors.length) ); if (!validLayers.length) { return null; @@ -187,7 +187,7 @@ export const buildExpression = ( hide: [Boolean(layer.hide)], - xAccessor: [layer.xAccessor], + xAccessor: layer.xAccessor ? [layer.xAccessor] : [], yScaleType: [ getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), ], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 2739ffe42f13f..8438b1f27dd0d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -7,13 +7,16 @@ import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { ArgumentType, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import chartAreaSVG from '../assets/chart_area.svg'; -import chartAreaStackedSVG from '../assets/chart_area_stacked.svg'; -import chartBarSVG from '../assets/chart_bar.svg'; -import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; -import chartBarHorizontalSVG from '../assets/chart_bar_horizontal.svg'; -import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked.svg'; -import chartLineSVG from '../assets/chart_line.svg'; +import { LensIconChartArea } from '../assets/chart_area'; +import { LensIconChartAreaStacked } from '../assets/chart_area_stacked'; +import { LensIconChartAreaPercentage } from '../assets/chart_area_percentage'; +import { LensIconChartBar } from '../assets/chart_bar'; +import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; +import { LensIconChartBarPercentage } from '../assets/chart_bar_percentage'; +import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; +import { LensIconChartBarHorizontalStacked } from '../assets/chart_bar_horizontal_stacked'; +import { LensIconChartBarHorizontalPercentage } from '../assets/chart_bar_horizontal_percentage'; +import { LensIconChartLine } from '../assets/chart_line'; import { VisualizationType } from '../index'; import { FittingFunction } from './fitting_functions'; @@ -230,7 +233,15 @@ export const layerConfig: ExpressionFunctionDefinition< }, seriesType: { types: ['string'], - options: ['bar', 'line', 'area', 'bar_stacked', 'area_stacked'], + options: [ + 'bar', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_percentage_stacked', + 'area_percentage_stacked', + ], help: 'The type of chart to display.', }, xScaleType: { @@ -283,8 +294,11 @@ export type SeriesType = | 'line' | 'area' | 'bar_stacked' + | 'bar_percentage_stacked' | 'bar_horizontal_stacked' - | 'area_stacked'; + | 'bar_horizontal_percentage_stacked' + | 'area_stacked' + | 'area_percentage_stacked'; export type YAxisMode = 'auto' | 'left' | 'right'; @@ -343,58 +357,72 @@ export type State = XYState; export const visualizationTypes: VisualizationType[] = [ { id: 'bar', - icon: 'visBarVertical', - largeIcon: chartBarSVG, + icon: LensIconChartBar, label: i18n.translate('xpack.lens.xyVisualization.barLabel', { defaultMessage: 'Bar', }), }, { id: 'bar_horizontal', - icon: 'visBarHorizontal', - largeIcon: chartBarHorizontalSVG, + icon: LensIconChartBarHorizontal, label: i18n.translate('xpack.lens.xyVisualization.barHorizontalLabel', { defaultMessage: 'Horizontal bar', }), }, { id: 'bar_stacked', - icon: 'visBarVerticalStacked', - largeIcon: chartBarStackedSVG, + icon: LensIconChartBarStacked, label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', { defaultMessage: 'Stacked bar', }), }, + { + id: 'bar_percentage_stacked', + icon: LensIconChartBarPercentage, + label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarLabel', { + defaultMessage: 'Bar percentage', + }), + }, { id: 'bar_horizontal_stacked', - icon: 'visBarHorizontalStacked', - largeIcon: chartBarHorizontalStackedSVG, + icon: LensIconChartBarHorizontalStacked, label: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalLabel', { defaultMessage: 'Stacked horizontal bar', }), }, { - id: 'line', - icon: 'visLine', - largeIcon: chartLineSVG, - label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { - defaultMessage: 'Line', + id: 'bar_horizontal_percentage_stacked', + icon: LensIconChartBarHorizontalPercentage, + label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel', { + defaultMessage: 'Horizontal bar percentage', }), }, { id: 'area', - icon: 'visArea', - largeIcon: chartAreaSVG, + icon: LensIconChartArea, label: i18n.translate('xpack.lens.xyVisualization.areaLabel', { defaultMessage: 'Area', }), }, { id: 'area_stacked', - icon: 'visAreaStacked', - largeIcon: chartAreaStackedSVG, + icon: LensIconChartAreaStacked, label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', { defaultMessage: 'Stacked area', }), }, + { + id: 'area_percentage_stacked', + icon: LensIconChartAreaPercentage, + label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageAreaLabel', { + defaultMessage: 'Area percentage', + }), + }, + { + id: 'line', + icon: LensIconChartLine, + label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { + defaultMessage: 'Line', + }), + }, ]; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index e80bb22c04a69..89a2574026ced 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -59,9 +59,11 @@ describe('XY Config panels', () => { expect(options!.map(({ id }) => id)).toEqual([ 'bar', 'bar_stacked', - 'line', + 'bar_percentage_stacked', 'area', 'area_stacked', + 'area_percentage_stacked', + 'line', ]); expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); @@ -83,7 +85,11 @@ describe('XY Config panels', () => { .first() .prop('options') as EuiButtonGroupProps['options']; - expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']); + expect(options!.map(({ id }) => id)).toEqual([ + 'bar_horizontal', + 'bar_horizontal_stacked', + 'bar_horizontal_percentage_stacked', + ]); expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index a2488d123e13a..62fd6e013f20d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -114,7 +114,6 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); }} isIconOnly - buttonSize="compressed" /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 31873228fb394..a8c7c8daaf58b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -17,6 +17,7 @@ import { Position, GeometryValue, XYChartSeriesIdentifier, + StackMode, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -229,11 +230,10 @@ export function XYChart({ const filteredLayers = layers.filter(({ layerId, xAccessor, accessors }) => { return !( - !xAccessor || !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || - data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined') + (xAccessor && data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) ); }); @@ -425,7 +425,7 @@ export function XYChart({ visible: gridlinesVisibilitySettings?.x, strokeWidth: 2, }} - hide={filteredLayers[0].hide} + hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor} tickFormat={(d) => xAxisFormatter.convert(d)} style={{ tickLabel: { @@ -483,8 +483,7 @@ export function XYChart({ // To not display them in the legend, they need to be filtered out. const rows = table.rows.filter( (row) => - xAccessor && - typeof row[xAccessor] !== 'undefined' && + !(xAccessor && typeof row[xAccessor] === 'undefined') && !( splitAccessor && typeof row[splitAccessor] === 'undefined' && @@ -492,21 +491,42 @@ export function XYChart({ ) ); + if (!xAccessor) { + rows.forEach((row) => { + row.unifiedX = i18n.translate('xpack.lens.xyChart.emptyXLabel', { + defaultMessage: '(empty)', + }); + }); + } + const seriesProps: SeriesSpec = { splitSeriesAccessors: splitAccessor ? [splitAccessor] : [], stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [], id: `${splitAccessor}-${accessor}`, - xAccessor, + xAccessor: xAccessor || 'unifiedX', yAccessors: [accessor], data: rows, - xScaleType, + xScaleType: xAccessor ? xScaleType : 'ordinal', yScaleType, color: () => getSeriesColor(layer, accessor), groupId: yAxesConfiguration.find((axisConfiguration) => axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) )?.groupId, enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor), + stackMode: seriesType.includes('percentage') ? StackMode.Percentage : undefined, timeZone, + areaSeriesStyle: { + point: { + visible: !xAccessor, + radius: 5, + }, + }, + lineSeriesStyle: { + point: { + visible: !xAccessor, + radius: 5, + }, + }, name(d) { const splitHint = table.columns.find((col) => col.id === splitAccessor)?.formatHint; @@ -545,10 +565,13 @@ export function XYChart({ ); case 'bar': case 'bar_stacked': + case 'bar_percentage_stacked': case 'bar_horizontal': case 'bar_horizontal_stacked': + case 'bar_horizontal_percentage_stacked': return ; case 'area_stacked': + case 'area_percentage_stacked': return ( ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 79e4ed6958193..ea5cff80695a3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -119,6 +119,51 @@ describe('xy_suggestions', () => { ); }); + test('suggests all xy charts without changes to the state when switching among xy charts with malformed table', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: false, + columns: [numCol('bytes')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: [], + state: { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'bar', + accessors: ['bytes'], + splitAccessor: undefined, + }, + { + layerId: 'second', + seriesType: 'bar', + accessors: ['bytes'], + splitAccessor: undefined, + }, + ], + }, + }); + + expect(suggestions).toHaveLength(visualizationTypes.length); + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ + 'bar', + 'bar_horizontal', + 'bar_stacked', + 'bar_percentage_stacked', + 'bar_horizontal_stacked', + 'bar_horizontal_percentage_stacked', + 'area', + 'area_stacked', + 'area_percentage_stacked', + 'line', + ]); + }); + test('suggests all basic x y charts when switching from another vis', () => { (generateId as jest.Mock).mockReturnValueOnce('aaa'); const suggestions = getSuggestions({ @@ -134,10 +179,13 @@ describe('xy_suggestions', () => { expect(suggestions).toHaveLength(visualizationTypes.length); expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'bar_stacked', + 'line', + 'area_percentage_stacked', 'area_stacked', 'area', - 'line', + 'bar_horizontal_percentage_stacked', 'bar_horizontal_stacked', + 'bar_percentage_stacked', 'bar_horizontal', 'bar', ]); @@ -157,13 +205,27 @@ describe('xy_suggestions', () => { }); expect(suggestions).toHaveLength(visualizationTypes.length); - expect(suggestions.map(({ state }) => state.layers.length)).toEqual([1, 1, 1, 1, 1, 1, 1]); + expect(suggestions.map(({ state }) => state.layers.length)).toEqual([ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ]); expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'bar_stacked', + 'line', + 'area_percentage_stacked', 'area_stacked', 'area', - 'line', + 'bar_horizontal_percentage_stacked', 'bar_horizontal_stacked', + 'bar_percentage_stacked', 'bar_horizontal', 'bar', ]); @@ -200,9 +262,12 @@ describe('xy_suggestions', () => { 'bar', 'bar_horizontal', 'bar_stacked', + 'bar_percentage_stacked', 'bar_horizontal_stacked', + 'bar_horizontal_percentage_stacked', 'area', 'area_stacked', + 'area_percentage_stacked', ]); }); @@ -244,9 +309,12 @@ describe('xy_suggestions', () => { 'bar', 'bar_horizontal', 'bar_stacked', + 'bar_percentage_stacked', 'bar_horizontal_stacked', + 'bar_horizontal_percentage_stacked', 'area', 'area_stacked', + 'area_percentage_stacked', ]); expect(suggestions.map(({ state }) => state.layers.map((l) => l.layerId))).toEqual([ ['first', 'second'], @@ -256,6 +324,9 @@ describe('xy_suggestions', () => { ['first', 'second'], ['first', 'second'], ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], ]); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 75dd5a7a579b8..42fc538874b93 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -45,6 +45,24 @@ export function getSuggestions({ table.columns.every((col) => col.operation.dataType !== 'number') || table.columns.some((col) => !columnSortOrder.hasOwnProperty(col.operation.dataType)) ) { + if (table.changeType === 'unchanged' && state) { + // this isn't a table we would switch to, but we have a state already. In this case, just use the current state for all series types + return visualizationTypes.map((visType) => { + const seriesType = visType.id as SeriesType; + return { + seriesType, + score: 0, + state: { + ...state, + preferredSeriesType: seriesType, + layers: state.layers.map((layer) => ({ ...layer, seriesType })), + }, + previewIcon: getIconForSeries(seriesType), + title: visType.label, + hide: true, + }; + }); + } return []; } @@ -91,6 +109,25 @@ function getSuggestionForColumns( } } +function flipSeriesType(seriesType: SeriesType) { + switch (seriesType) { + case 'bar_horizontal': + return 'bar'; + case 'bar_horizontal_stacked': + return 'bar_stacked'; + case 'bar': + return 'bar_horizontal'; + case 'bar_horizontal_stacked': + return 'bar_stacked'; + case 'bar_horizontal_percentage_stacked': + return 'bar_percentage_stacked'; + case 'bar_percentage_stacked': + return 'bar_horizontal_percentage_stacked'; + default: + return 'bar_horizontal'; + } +} + function getBucketMappings(table: TableSuggestion, currentState?: State) { const currentLayer = currentState && currentState.layers.find(({ layerId }) => layerId === table.layerId); @@ -156,7 +193,7 @@ function getSuggestionsForLayer({ }: { layerId: string; changeType: TableChangeType; - xValue: TableSuggestionColumn; + xValue?: TableSuggestionColumn; yValues: TableSuggestionColumn[]; splitBy?: TableSuggestionColumn; currentState?: State; @@ -164,7 +201,7 @@ function getSuggestionsForLayer({ keptLayerIds: string[]; }): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); - const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType); + const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue); const options = { currentState, @@ -182,11 +219,13 @@ function getSuggestionsForLayer({ if (!currentState && changeType === 'unchanged') { // Chart switcher needs to include every chart type return visualizationTypes - .map((visType) => ({ - ...buildSuggestion({ ...options, seriesType: visType.id as SeriesType }), - title: visType.label, - hide: visType.id !== 'bar_stacked', - })) + .map((visType) => { + return { + ...buildSuggestion({ ...options, seriesType: visType.id as SeriesType }), + title: visType.label, + hide: visType.id !== 'bar_stacked', + }; + }) .sort((a, b) => (a.state.preferredSeriesType === 'bar_stacked' ? -1 : 1)); } @@ -199,18 +238,13 @@ function getSuggestionsForLayer({ const sameStateSuggestions: Array> = []; // if current state is using the same data, suggest same chart with different presentational configuration - if (seriesType !== 'line' && xValue.operation.scale === 'ordinal') { + if (seriesType.includes('bar') && (!xValue || xValue.operation.scale === 'ordinal')) { // flip between horizontal/vertical for ordinal scales sameStateSuggestions.push( buildSuggestion({ ...options, title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }), - seriesType: - seriesType === 'bar_horizontal' - ? 'bar' - : seriesType === 'bar_horizontal_stacked' - ? 'bar_stacked' - : 'bar_horizontal', + seriesType: flipSeriesType(seriesType), }) ); } else { @@ -231,7 +265,7 @@ function getSuggestionsForLayer({ ); } - if (seriesType !== 'line' && splitBy) { + if (seriesType !== 'line' && splitBy && !seriesType.includes('percentage')) { // flip between stacked/unstacked sameStateSuggestions.push( buildSuggestion({ @@ -248,6 +282,30 @@ function getSuggestionsForLayer({ ); } + if ( + seriesType !== 'line' && + seriesType.includes('stacked') && + !seriesType.includes('percentage') + ) { + const percentageOptions = { ...options }; + if (percentageOptions.xValue?.operation.scale === 'ordinal' && !percentageOptions.splitBy) { + percentageOptions.splitBy = percentageOptions.xValue; + delete percentageOptions.xValue; + } + // percentage suggestion + sameStateSuggestions.push( + buildSuggestion({ + ...options, + // hide the suggestion if split by is missing + hide: !percentageOptions.splitBy, + seriesType: asPercentageSeriesType(seriesType), + title: i18n.translate('xpack.lens.xySuggestions.asPercentageTitle', { + defaultMessage: 'Percentage', + }), + }) + ); + } + // Combine all pre-built suggestions with hidden suggestions for remaining chart types return sameStateSuggestions.concat( visualizationTypes @@ -280,6 +338,19 @@ function toggleStackSeriesType(oldSeriesType: SeriesType) { } } +function asPercentageSeriesType(oldSeriesType: SeriesType) { + switch (oldSeriesType) { + case 'area_stacked': + return 'area_percentage_stacked'; + case 'bar_stacked': + return 'bar_percentage_stacked'; + case 'bar_horizontal_stacked': + return 'bar_horizontal_percentage_stacked'; + default: + return oldSeriesType; + } +} + // Until the area chart rendering bug is fixed, avoid suggesting area charts // https://github.com/elastic/elastic-charts/issues/388 function altSeriesType(oldSeriesType: SeriesType) { @@ -301,8 +372,7 @@ function altSeriesType(oldSeriesType: SeriesType) { function getSeriesType( currentState: XYState | undefined, layerId: string, - xValue: TableSuggestionColumn, - changeType: TableChangeType + xValue?: TableSuggestionColumn ): SeriesType { const defaultType = 'bar_stacked'; @@ -314,7 +384,7 @@ function getSeriesType( // Attempt to keep the seriesType consistent on initial add of a layer // Ordinal scales should always use a bar because there is no interpolation between buckets - if (xValue.operation.scale && xValue.operation.scale === 'ordinal') { + if (xValue && xValue.operation.scale && xValue.operation.scale === 'ordinal') { return closestSeriesType.startsWith('bar') ? closestSeriesType : defaultType; } @@ -323,7 +393,7 @@ function getSeriesType( function getSuggestionTitle( yValues: TableSuggestionColumn[], - xValue: TableSuggestionColumn, + xValue: TableSuggestionColumn | undefined, tableLabel: string | undefined ) { const yTitle = yValues @@ -335,10 +405,14 @@ function getSuggestionTitle( 'A character that can be used for conjunction of multiple enumarated items. Make sure to include spaces around it if needed.', }) ); - const xTitle = xValue.operation.label; + const xTitle = + xValue?.operation.label || + i18n.translate('xpack.lens.xySuggestions.emptyAxisTitle', { + defaultMessage: '(empty)', + }); const title = tableLabel || - (xValue.operation.dataType === 'date' + (xValue?.operation.dataType === 'date' ? i18n.translate('xpack.lens.xySuggestions.dateSuggestion', { defaultMessage: '{yTitle} over {xTitle}', description: @@ -364,24 +438,30 @@ function buildSuggestion({ changeType, xValue, keptLayerIds, + hide, }: { currentState: XYState | undefined; seriesType: SeriesType; title: string; yValues: TableSuggestionColumn[]; - xValue: TableSuggestionColumn; + xValue?: TableSuggestionColumn; splitBy: TableSuggestionColumn | undefined; layerId: string; changeType: TableChangeType; keptLayerIds: string[]; + hide?: boolean; }) { + if (seriesType.includes('percentage') && xValue?.operation.scale === 'ordinal' && !splitBy) { + splitBy = xValue; + xValue = undefined; + } const existingLayer: LayerConfig | {} = getExistingLayer(currentState, layerId) || {}; const accessors = yValues.map((col) => col.columnId); const newLayer = { ...existingLayer, layerId, seriesType, - xAccessor: xValue.columnId, + xAccessor: xValue?.columnId, splitAccessor: splitBy?.columnId, accessors, yConfig: @@ -427,10 +507,11 @@ function buildSuggestion({ title, score: getScore(yValues, splitBy, changeType), hide: + hide ?? // Only advertise very clear changes when XY chart is not active - (!currentState && changeType !== 'unchanged' && changeType !== 'extended') || - // Don't advertise removing dimensions - (currentState && changeType === 'reduced'), + ((!currentState && changeType !== 'unchanged' && changeType !== 'extended') || + // Don't advertise removing dimensions + (currentState && changeType === 'reduced')), state, previewIcon: getIconForSeries(seriesType), }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 53f7a23dcae98..7cf1830cdc2fa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,6 +9,7 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; +import { LensIconChartBar } from '../assets/chart_bar'; function exampleState(): State { return { @@ -49,9 +50,7 @@ describe('xy_visualization', () => { it('should show the preferredSeriesType if there are no layers', () => { const desc = xyVisualization.getDescription(mixedState()); - // 'test-file-stub' is a hack, but it at least means we aren't using - // a standard icon here. - expect(desc.icon).toEqual('test-file-stub'); + expect(desc.icon).toEqual(LensIconChartBar); expect(desc.label).toEqual('Bar chart'); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index 8c551c575764e..0b8f7e2ed0f11 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -14,12 +14,13 @@ import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; -import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; -import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression } from './to_expression'; +import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; +import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; +import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; -const defaultIcon = chartBarStackedSVG; +const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; @@ -48,29 +49,27 @@ function getDescription(state?: State) { const visualizationType = getVisualizationType(state); - if (!state.layers.length) { - const preferredType = visualizationType as VisualizationType; + if (visualizationType === 'mixed' && isHorizontalChart(state.layers)) { + return { + icon: LensIconChartBarHorizontal, + label: i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed horizontal bar', + }), + }; + } + + if (visualizationType === 'mixed') { return { - icon: preferredType.largeIcon || preferredType.icon, - label: preferredType.label, + icon: LensIconChartMixedXy, + label: i18n.translate('xpack.lens.xyVisualization.mixedLabel', { + defaultMessage: 'Mixed XY', + }), }; } return { - icon: - visualizationType === 'mixed' - ? chartMixedSVG - : visualizationType.largeIcon || visualizationType.icon, - label: - visualizationType === 'mixed' - ? isHorizontalChart(state.layers) - ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed horizontal bar', - }) - : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY', - }) - : visualizationType.label, + icon: visualizationType.icon, + label: visualizationType.label, }; } @@ -172,7 +171,7 @@ export const xyVisualization: Visualization = { filterOperations: isBucketed, suggestedPriority: 1, supportsMoreColumns: !layer.xAccessor, - required: true, + required: !layer.seriesType.includes('percentage'), dataTestSubj: 'lnsXY_xDimensionPanel', }, { @@ -197,6 +196,7 @@ export const xyVisualization: Visualization = { suggestedPriority: 0, supportsMoreColumns: !layer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', + required: layer.seriesType.includes('percentage'), }, ], }; diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 7b5521443d974..f220f32d346e7 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -17,3 +17,5 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; + +export { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts new file mode 100644 index 0000000000000..791a7de48f36f --- /dev/null +++ b/x-pack/plugins/ml/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SearchResponse7 } from './types/es_client'; diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts new file mode 100644 index 0000000000000..d9ca9a3b584ab --- /dev/null +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse, ShardsResponse } from 'elasticsearch'; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7Hits { + hits: SearchResponse['hits']['hits']; + max_score: number; + total: { + value: number; + relation: string; + }; +} +export interface SearchResponse7 { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: ShardsResponse; + hits: SearchResponse7Hits; + aggregations?: any; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index 4bbd3595e5a7e..633d70687dd27 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -19,7 +19,6 @@ export { DataGridItem, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, } from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index f9ee8c37fabf7..22fff0f6e0b93 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -5,7 +5,6 @@ */ import { Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; @@ -43,16 +42,6 @@ export type EsSorting = Dictionary<{ order: 'asc' | 'desc'; }>; -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -export interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - export interface UseIndexDataReturnType extends Pick< UseDataGridReturnType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 53c0f02fd9a80..361a79d42214d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import type { SearchResponse7 } from '../../../../common/types/es_client'; import { extractErrorMessage } from '../../../../common/util/errors'; -import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { EsSorting, UseDataGridReturnType } from '../../components/data_grid'; import { ml } from '../../services/ml_api_service'; import { isKeywordAndTextType } from '../common/fields'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index ea958c8c4a3a3..74d45b86c8c4d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -22,9 +22,9 @@ import { useDataGrid, useRenderCellValue, EsSorting, - SearchResponse7, UseIndexDataReturnType, } from '../../../../components/data_grid'; +import type { SearchResponse7 } from '../../../../../../common/types/es_client'; import { extractErrorMessage } from '../../../../../../common/util/errors'; import { INDEX_STATUS } from '../../../common/analytics'; import { ml } from '../../../../services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index da5caf8e3875a..e72af6a0e30c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -21,7 +21,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useMlContext } from '../../../contexts/ml'; -import { newJobCapsService } from '../../../services/new_job_capabilities_service'; import { ml } from '../../../services/ml_api_service'; import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form'; import { CreateAnalyticsAdvancedEditor } from './components/create_analytics_advanced_editor'; @@ -62,8 +61,6 @@ export const Page: FC = ({ jobId }) => { if (currentIndexPattern) { (async function () { - await newJobCapsService.initializeFromIndexPattern(currentIndexPattern, false, false); - if (jobId !== undefined) { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); if ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index 8c45398098b2f..4ce2abf3fef60 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -16,6 +16,7 @@ import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', @@ -36,7 +37,10 @@ const PageWrapper: FC = ({ location, deps }) => { sort: false, }); - const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); + const { context } = useResolver(index, savedSearchId, deps.config, { + ...basicResolvers(deps), + jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), + }); return ( diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index 2912aad6819cf..6a5583ecbb8ac 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -61,8 +61,13 @@ describe('ML - custom URL utils', () => { influencer_field_name: 'airline', influencer_field_values: ['<>:;[}")'], }, + { + influencer_field_name: 'odd:field,name', + influencer_field_values: [">:&12<'"], + }, ], airline: ['<>:;[}")'], + 'odd:field,name': [">:&12<'"], }; const TEST_RECORD_MULTIPLE_INFLUENCER_VALUES: CustomUrlAnomalyRecordDoc = { @@ -98,7 +103,7 @@ describe('ML - custom URL utils', () => { url_name: 'Raw data', time_range: 'auto', url_value: - "discover#/?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"$airline$\"'))", + "discover#/?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"$airline$\" and odd:field,name : $odd:field,name$'))", }; const TEST_DASHBOARD_LUCENE_URL: KibanaUrlConfig = { @@ -263,9 +268,55 @@ describe('ML - custom URL utils', () => { ); }); - test('returns expected URL for a Kibana Discover type URL when record field contains special characters', () => { + test.skip('returns expected URL for a Kibana Discover type URL when record field contains special characters', () => { expect(getUrlForRecord(TEST_DISCOVER_URL, TEST_RECORD_SPECIAL_CHARS)).toBe( - "discover#/?_g=(time:(from:'2017-02-09T15:10:00.000Z',mode:absolute,to:'2017-02-09T17:15:00.000Z'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"%3C%3E%3A%3B%5B%7D%5C%22)\"'))" + "discover#/?_g=(time:(from:'2017-02-09T15:10:00.000Z',mode:absolute,to:'2017-02-09T17:15:00.000Z'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"%3C%3E%3A%3B%5B%7D%5C%22)\" and odd:field,name:>:&12<''))" + ); + }); + + test('correctly encodes special characters inside of a query string', () => { + const testUrl = { + url_name: 'Show dashboard', + time_range: 'auto', + url_value: `dashboards#/view/351de820-f2bb-11ea-ab06-cb93221707e9?_a=(filters:!(),query:(language:kuery,query:'at@name:"$at@name$" and singlequote!'name:"$singlequote!'name$"'))&_g=(filters:!(),time:(from:'$earliest$',mode:absolute,to:'$latest$'))`, + }; + + const testRecord = { + job_id: 'spec-char', + result_type: 'record', + probability: 0.0028099428534745633, + multi_bucket_impact: 5, + record_score: 49.00785814424704, + initial_record_score: 49.00785814424704, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1549593000000, + partition_field_name: 'at@name', + partition_field_value: "contains a ' quote", + function: 'mean', + function_description: 'mean', + typical: [1993.2657340111837], + actual: [1808.3334418402778], + field_name: 'metric%$£&!{(]field', + influencers: [ + { + influencer_field_name: "singlequote'name", + influencer_field_values: ["contains a ' quote"], + }, + { + influencer_field_name: 'at@name', + influencer_field_values: ["contains a ' quote"], + }, + ], + "singlequote'name": ["contains a ' quote"], + 'at@name': ["contains a ' quote"], + earliest: '2019-02-08T00:00:00.000Z', + latest: '2019-02-08T23:59:59.999Z', + }; + + expect(getUrlForRecord(testUrl, testRecord)).toBe( + `dashboards#/view/351de820-f2bb-11ea-ab06-cb93221707e9?_a=(filters:!(),query:(language:kuery,query:'at@name:"contains%20a%20!'%20quote" AND singlequote!'name:"contains%20a%20!'%20quote"'))&_g=(filters:!(),time:(from:'2019-02-08T00:00:00.000Z',mode:absolute,to:'2019-02-08T23:59:59.999Z'))` ); }); @@ -405,6 +456,58 @@ describe('ML - custom URL utils', () => { ); }); + test('return expected url for Security app', () => { + const urlConfig = { + url_name: 'Hosts Details by process name', + url_value: + "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))", + }; + + const testRecords = { + job_id: 'rare_process_by_host_linux_ecs', + result_type: 'record', + probability: 0.018122957282324745, + multi_bucket_impact: 0, + record_score: 20.513469583273547, + initial_record_score: 20.513469583273547, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1549043100000, + by_field_name: 'process.name', + by_field_value: 'seq', + partition_field_name: 'host.name', + partition_field_value: 'showcase', + function: 'rare', + function_description: 'rare', + typical: [0.018122957282324745], + actual: [1], + influencers: [ + { + influencer_field_name: 'user.name', + influencer_field_values: ['sophie'], + }, + { + influencer_field_name: 'process.name', + influencer_field_values: ['seq'], + }, + { + influencer_field_name: 'host.name', + influencer_field_values: ['showcase'], + }, + ], + 'process.name': ['seq'], + 'user.name': ['sophie'], + 'host.name': ['showcase'], + earliest: '2019-02-01T16:00:00.000Z', + latest: '2019-02-01T18:59:59.999Z', + }; + + expect(getUrlForRecord(urlConfig, testRecords)).toBe( + "security/hosts/ml-hosts/showcase?_g=()&query=(language:kuery,query:'process.name:\"seq\"')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-02-01T16:00:00.000Z',kind:absolute,to:'2019-02-01T18:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-02-01T16%3A00%3A00.000Z',kind:absolute,to:'2019-02-01T18%3A59%3A59.999Z')))" + ); + }); + test('removes an empty path component with a trailing slash', () => { const urlConfig = { url_name: 'APM', diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 8263def2034aa..18ba1e4ee337b 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -8,6 +8,7 @@ import { get, flow } from 'lodash'; import moment from 'moment'; +import rison, { RisonObject, RisonValue } from 'rison-node'; import { parseInterval } from '../../../common/util/parse_interval'; import { escapeForElasticsearchQuery, replaceStringTokens } from './string_utils'; @@ -131,13 +132,70 @@ function escapeForKQL(value: string | number): string { type GetResultTokenValue = (v: string) => string; +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return value !== null && typeof value === 'object'; +}; + +const getQueryStringResultProvider = ( + record: CustomUrlAnomalyRecordDoc, + getResultTokenValue: GetResultTokenValue +) => (resultPrefix: string, queryString: string, resultPostfix: string): string => { + const URL_LENGTH_LIMIT = 2000; + + let availableCharactersLeft = URL_LENGTH_LIMIT - resultPrefix.length - resultPostfix.length; + + // URL template might contain encoded characters + const queryFields = queryString + // Split query string by AND operator. + .split(/\sand\s/i) + // Get property name from `influencerField:$influencerField$` string. + .map((v) => String(v.split(/:(.+)?\$/)[0]).trim()); + + const queryParts: string[] = []; + const joinOperator = ' AND '; + + fieldsLoop: for (let i = 0; i < queryFields.length; i++) { + const field = queryFields[i]; + // Use lodash get to allow nested JSON fields to be retrieved. + let tokenValues: string[] | string | null = get(record, field) || null; + if (tokenValues === null) { + continue; + } + tokenValues = Array.isArray(tokenValues) ? tokenValues : [tokenValues]; + + // Create a pair `influencerField:value`. + // In cases where there are multiple influencer field values for an anomaly + // combine values with OR operator e.g. `(influencerField:value or influencerField:another_value)`. + let result = ''; + for (let j = 0; j < tokenValues.length; j++) { + const part = `${j > 0 ? ' OR ' : ''}${field}:"${getResultTokenValue(tokenValues[j])}"`; + + // Build up a URL string which is not longer than the allowed length and isn't corrupted by invalid query. + if (availableCharactersLeft < part.length) { + if (result.length > 0) { + queryParts.push(j > 0 ? `(${result})` : result); + } + break fieldsLoop; + } + + result += part; + + availableCharactersLeft -= result.length; + } + + if (result.length > 0) { + queryParts.push(tokenValues.length > 1 ? `(${result})` : result); + } + } + return queryParts.join(joinOperator); +}; + /** * Builds a Kibana dashboard or Discover URL from the supplied config, with any * dollar delimited tokens substituted from the supplied anomaly record. */ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) { const urlValue = urlConfig.url_value; - const URL_LENGTH_LIMIT = 2000; const isLuceneQueryLanguage = urlValue.includes('language:lucene'); @@ -145,11 +203,7 @@ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) ? escapeForElasticsearchQuery : escapeForKQL; - const commonEscapeCallback = flow( - // Kibana URLs used rison encoding, so escape with ! any ! or ' characters - (v: string): string => v.replace(/[!']/g, '!$&'), - encodeURIComponent - ); + const commonEscapeCallback = flow(encodeURIComponent); const replaceSingleTokenValues = (str: string) => { const getResultTokenValue: GetResultTokenValue = flow( @@ -172,65 +226,34 @@ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) return flow( (str: string) => str.replace('$earliest$', record.earliest).replace('$latest$', record.latest), // Process query string content of the URL + decodeURIComponent, (str: string) => { const getResultTokenValue: GetResultTokenValue = flow( queryLanguageEscapeCallback, commonEscapeCallback ); + + const getQueryStringResult = getQueryStringResultProvider(record, getResultTokenValue); + + const match = str.match(/(.+)(\(.*\blanguage:(?:lucene|kuery)\b.*?\))(.+)/); + + if (match !== null && match[2] !== undefined) { + const [, prefix, queryDef, postfix] = match; + + const q = rison.decode(queryDef); + + if (isRisonObject(q) && q.hasOwnProperty('query')) { + const [resultPrefix, resultPostfix] = [prefix, postfix].map(replaceSingleTokenValues); + const resultQuery = getQueryStringResult(resultPrefix, q.query as string, resultPostfix); + return `${resultPrefix}${rison.encode({ ...q, query: resultQuery })}${resultPostfix}`; + } + } + return str.replace( - /(.+query:'|.+&kuery=)([^']*)(['&].+)/, + /(.+&kuery=)(.*?)[^!](&.+)/, (fullMatch, prefix: string, queryString: string, postfix: string) => { const [resultPrefix, resultPostfix] = [prefix, postfix].map(replaceSingleTokenValues); - - let availableCharactersLeft = - URL_LENGTH_LIMIT - resultPrefix.length - resultPostfix.length; - const queryFields = queryString - // Split query string by AND operator. - .split(/\sand\s/i) - // Get property name from `influencerField:$influencerField$` string. - .map((v) => v.split(':')[0]); - - const queryParts: string[] = []; - const joinOperator = ' AND '; - - fieldsLoop: for (let i = 0; i < queryFields.length; i++) { - const field = queryFields[i]; - // Use lodash get to allow nested JSON fields to be retrieved. - let tokenValues: string[] | string | null = get(record, field) || null; - if (tokenValues === null) { - continue; - } - tokenValues = Array.isArray(tokenValues) ? tokenValues : [tokenValues]; - - // Create a pair `influencerField:value`. - // In cases where there are multiple influencer field values for an anomaly - // combine values with OR operator e.g. `(influencerField:value or influencerField:another_value)`. - let result = ''; - for (let j = 0; j < tokenValues.length; j++) { - const part = `${j > 0 ? ' OR ' : ''}${field}:"${getResultTokenValue( - tokenValues[j] - )}"`; - - // Build up a URL string which is not longer than the allowed length and isn't corrupted by invalid query. - if (availableCharactersLeft < part.length) { - if (result.length > 0) { - queryParts.push(j > 0 ? `(${result})` : result); - } - break fieldsLoop; - } - - result += part; - - availableCharactersLeft -= result.length; - } - - if (result.length > 0) { - queryParts.push(tokenValues.length > 1 ? `(${result})` : result); - } - } - - const resultQuery = queryParts.join(joinOperator); - + const resultQuery = getQueryStringResult(resultPrefix, queryString, resultPostfix); return `${resultPrefix}${resultQuery}${resultPostfix}`; } ); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 016acf2737f9b..f583b4882f83c 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -9,6 +9,7 @@ import { ILegacyCustomClusterClient, Logger, IUiSettingsClient, + LegacyCallAPIOptions, } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { @@ -36,6 +37,8 @@ import { MonitoringConfig } from '../config'; import { AlertSeverity } from '../../common/enums'; import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; import { MonitoringLicenseService } from '../types'; +import { mbSafeQuery } from '../lib/mb_safe_query'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; export class BaseAlert { public type!: string; @@ -212,13 +215,20 @@ export class BaseAlert { `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); - const callCluster = this.monitoringCluster + const _callCluster = this.monitoringCluster ? this.monitoringCluster.callAsInternalUser : services.callCluster; + const callCluster = async ( + endpoint: string, + clientParams?: Record, + options?: LegacyCallAPIOptions + ) => { + return await mbSafeQuery(async () => _callCluster(endpoint, clientParams, options)); + }; const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; // Support CCS use cases by querying to find available remote clusters // and then adding those to the index pattern we are searching against - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 4b083787f58cb..66085b53516a2 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -66,7 +66,11 @@ describe('ClusterHealthAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index c330e977e53d8..2705a77e0fce4 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -70,7 +70,11 @@ describe('CpuUsageAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index afe5abcf1ebd7..5bca84e33da3c 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -31,6 +31,7 @@ import { CommonAlertParams, CommonAlertParamDetail, } from '../../common/types'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; const RESOLVED = i18n.translate('xpack.monitoring.alerts.cpuUsage.resolved', { defaultMessage: 'resolved', @@ -137,7 +138,7 @@ export class CpuUsageAlert extends BaseAlert { uiSettings: IUiSettingsClient, availableCcs: string[] ): Promise { - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index ed300c211215b..1db85f915d794 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -69,7 +69,11 @@ describe('ElasticsearchVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index dd3b37b5755e7..362532a995f2d 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -72,7 +72,11 @@ describe('KibanaVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index e2f21b34efe21..da94e4af83802 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -76,7 +76,11 @@ describe('LicenseExpirationAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index fbb4a01d5b4ed..5ed189014cc6e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -69,7 +69,11 @@ describe('LogstashVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 4b3e3d2d6cb6d..ec2b19eb5dfae 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -82,7 +82,11 @@ describe('NodesChangedAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts b/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts new file mode 100644 index 0000000000000..683a0dfeccb1f --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MonitoringConfig } from '../../config'; + +export function appendMetricbeatIndex(config: MonitoringConfig, indexPattern: string) { + return `${indexPattern},${config.ui.metricbeat.index}`; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts index 7fdbc79685f7c..1907d2b4b3401 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { - return `${indexPattern},${indexPattern - .split(',') - .map((pattern) => { - return remotes.map((remoteName) => `${remoteName}:${pattern}`).join(','); - }) - .join(',')}`; + const patternsToAdd = []; + for (const index of indexPattern.split(',')) { + for (const remote of remotes) { + patternsToAdd.push(`${remote}:${index}`); + } + } + return [...indexPattern.split(','), ...patternsToAdd].join(','); } diff --git a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js index f08f92bffe790..37e739d0066a0 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js +++ b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js @@ -26,6 +26,7 @@ export async function getTimeOfLastEvent({ { timestamp: { order: 'desc', + unmapped_type: 'long', }, }, ], diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js index cc3682ef764c8..0b2e833933177 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js @@ -73,7 +73,7 @@ export async function getApmInfo(req, apmIndexPattern, { clusterUuid, apmUuid, s 'hits.hits.inner_hits.first_hit.hits.hits._source.beats_stats.metrics.libbeat.output.write.bytes', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ start, end, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js index 19ed8298391d7..03a395e87d860 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js @@ -128,8 +128,8 @@ export async function getApms(req, apmIndexPattern, clusterUuid) { }, }, sort: [ - { 'beats_stats.beat.uuid': { order: 'asc' } }, // need to keep duplicate uuids grouped - { timestamp: { order: 'desc' } }, // need oldest timestamp to come first for rate calcs to work + { 'beats_stats.beat.uuid': { order: 'asc', unmapped_type: 'long' } }, // need to keep duplicate uuids grouped + { timestamp: { order: 'desc', unmapped_type: 'long' } }, // need oldest timestamp to come first for rate calcs to work ], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js index 30ec728546ce9..962018f88354d 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js @@ -78,7 +78,7 @@ export async function getBeatSummary( 'hits.hits.inner_hits.first_hit.hits.hits._source.beats_stats.metrics.libbeat.output.write.bytes', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createBeatsQuery({ start, end, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js index a5d43d1da7ebc..af4b6c31a3e5e 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js @@ -126,8 +126,8 @@ export async function getBeats(req, beatsIndexPattern, clusterUuid) { }, }, sort: [ - { 'beats_stats.beat.uuid': { order: 'asc' } }, // need to keep duplicate uuids grouped - { timestamp: { order: 'desc' } }, // need oldest timestamp to come first for rate calcs to work + { 'beats_stats.beat.uuid': { order: 'asc', unmapped_type: 'long' } }, // need to keep duplicate uuids grouped + { timestamp: { order: 'desc', unmapped_type: 'long' } }, // need oldest timestamp to come first for rate calcs to work ], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js index bef07124fb430..96910dd86a94d 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js @@ -13,7 +13,7 @@ export function appendMetricbeatIndex(config, indexPattern) { if (isFunction(config.get)) { mbIndex = config.get('monitoring.ui.metricbeat.index'); } else { - mbIndex = get(config, 'monitoring.ui.metricbeat.index'); + mbIndex = get(config, 'ui.metricbeat.index'); } const newIndexPattern = `${indexPattern},${mbIndex}`; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js index 8e0d125d122aa..a1674b2f5eb36 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js @@ -31,7 +31,7 @@ async function findSupportedBasicLicenseCluster( ignoreUnavailable: true, filterPath: 'hits.hits._source.cluster_uuid', body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter: [ diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js index a167837969bd0..bd84fbb66f962 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js @@ -18,7 +18,7 @@ export function getClusterLicense(req, esIndexPattern, clusterUuid) { ignoreUnavailable: true, filterPath: 'hits.hits._source.license', body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'cluster_stats', clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js index 33e4ec96676b2..fa5526728086e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js @@ -70,7 +70,7 @@ export function getClustersState(req, esIndexPattern, clusters) { collapse: { field: 'cluster_uuid', }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js index 945bf1f2e19a2..8ddd33837f56e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js @@ -67,7 +67,7 @@ function fetchClusterStats(req, esIndexPattern, clusterUuid) { collapse: { field: 'cluster_uuid', }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js index 209a48cce369c..0f0ba49f229b0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js @@ -37,7 +37,7 @@ export async function checkCcrEnabled(req, esIndexPattern) { clusterUuid, metric: metricFields, }), - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, filterPath: ['hits.hits._source.stack_stats.xpack.ccr'], }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js index db8c89c364463..00e750b17d57b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js @@ -61,7 +61,7 @@ export function getLastRecovery(req, esIndexPattern) { ignoreUnavailable: true, body: { _source: ['index_recovery.shards'], - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'index_recovery', start, end, clusterUuid, metric }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js index 74d4bd6d2b5df..71f3633406c9b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js @@ -42,7 +42,7 @@ export function getMlJobs(req, esIndexPattern) { 'hits.hits._source.job_stats.node.name', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, collapse: { field: 'job_stats.job_id' }, query: createQuery({ type: 'job_stats', start, end, clusterUuid, metric }), }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js index 524eaca191eec..6a0935f2b2d67 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js @@ -69,7 +69,7 @@ export function getIndexSummary( size: 1, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'index_stats', start, end, clusterUuid, metric, filters }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js index ba6d0cb926f06..cc3dec9f085b7 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js @@ -129,7 +129,7 @@ export function buildGetIndicesQuery( sort: [{ timestamp: 'asc' }], }, }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js index 84384021a3593..06f5d5488a1ae 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js @@ -109,7 +109,7 @@ export function getNodeSummary( size: 1, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'node_stats', start, end, clusterUuid, metric, filters }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js index c2794b7e7fa44..3766845d39b4f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js @@ -92,7 +92,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n }, }, }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, filterPath: [ 'hits.hits._source.source_node', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js index e8728e9c53ec5..f39233b29a1ce 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js @@ -20,7 +20,7 @@ async function getUnassignedShardData(req, esIndexPattern, cluster) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js index 7823884dc749d..41a4740675637 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js @@ -19,7 +19,7 @@ async function getShardCountPerNode(req, esIndexPattern, cluster) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js index 1154655ab6a22..2ac1e99add4de 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js @@ -57,7 +57,7 @@ export function getShardStats( size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js index 533354f1e27b3..5a3e2dea930e0 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js @@ -41,7 +41,7 @@ export function getKibanaInfo(req, kbnIndexPattern, { clusterUuid, kibanaUuid }) }, }, collapse: { field: 'kibana_stats.kibana.uuid' }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js index f0e3f961a498f..b65f7770119fc 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js @@ -44,7 +44,7 @@ export function getKibanas(req, kbnIndexPattern, { clusterUuid }) { collapse: { field: 'kibana_stats.kibana.uuid', }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], _source: [ 'timestamp', 'kibana_stats.process.memory.resident_set_size_in_bytes', diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js index fd7b5d457409f..7947a5b6797ae 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js +++ b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js @@ -65,7 +65,7 @@ export async function getLogTypes( filterPath: ['aggregations.levels.buckets', 'aggregations.types.buckets'], ignoreUnavailable: true, body: { - sort: { '@timestamp': { order: 'desc' } }, + sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter, diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_logs.js b/x-pack/plugins/monitoring/server/lib/logs/get_logs.js index bb453e09454af..7952bc02b91c2 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_logs.js +++ b/x-pack/plugins/monitoring/server/lib/logs/get_logs.js @@ -82,7 +82,7 @@ export async function getLogs( ], ignoreUnavailable: true, body: { - sort: { '@timestamp': { order: 'desc' } }, + sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js index 929dd53f74776..fdfc523e53527 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js @@ -46,7 +46,7 @@ export function getNodeInfo(req, lsIndexPattern, { clusterUuid, logstashUuid }) }, }, collapse: { field: 'logstash_stats.logstash.uuid' }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js index 57adaff9be1c4..9b8786f8ae017 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js @@ -44,7 +44,7 @@ export function getNodes(req, lsIndexPattern, { clusterUuid }) { collapse: { field: 'logstash_stats.logstash.uuid', }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], _source: [ 'timestamp', 'logstash_stats.process.cpu.percent', diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js index d844e3604ca79..dae8d52e6c57b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js @@ -37,7 +37,7 @@ export async function getPipelineStateDocument( ignoreUnavailable: true, body: { _source: { excludes: 'logstash_state.pipeline.representation.plugins' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query, terminate_after: 1, // Safe to do because all these documents are functionally identical }, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js index 91ac158b22494..c51f0f3ea1c03 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js @@ -83,7 +83,7 @@ function fetchPipelineVersions(...args) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query, aggs, }, diff --git a/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts b/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts new file mode 100644 index 0000000000000..86bf5de8601e0 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * This function is designed to enable us to query against `.monitoring-*` and `metricbeat-*` + * indices SAFELY. We are adding the proper aliases into `metricbeat-*` to ensure all existing + * queries/aggs continue to work but we need to handle the reality that these aliases will not + * exist for older metricbeat-* indices, created before the aliases existed. + * + * Certain parts of a query will fail in this scenario, throwing an exception because of unmapped fields. + * So far, this is known to affect `sort` and `collapse` search query parameters. We have a way + * to handle this error elegantly with `sort` but not with `collapse` so we handle it manually in this spot. + * + * We can add future edge cases in here as well. + * + * @param queryExecutor + */ +export const mbSafeQuery = async (queryExecutor: () => Promise) => { + try { + return await queryExecutor(); + } catch (err) { + if ( + err.message.includes('no mapping found for') && + err.message.includes('in order to collapse on') + ) { + return {}; + } + throw err; + } +}; diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index c7197d1aa655a..8b37ae9d5dce3 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -34,6 +34,7 @@ import { requireUIRoutes } from './routes'; import { initBulkUploader } from './kibana_monitoring'; // @ts-ignore import { initInfraSource } from './lib/logs/init_infra_source'; +import { mbSafeQuery } from './lib/mb_safe_query'; import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; import { registerMonitoringCollection } from './telemetry_collection'; @@ -354,7 +355,9 @@ export class Plugin { callWithRequest: async (_req: any, endpoint: string, params: any) => { const client = name === 'monitoring' ? cluster : this.legacyShimDependencies.esDataClient; - return client.asScoped(req).callAsCurrentUser(endpoint, params); + return mbSafeQuery(() => + client.asScoped(req).callAsCurrentUser(endpoint, params) + ); }, }), }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js index 9999ba774b28d..fbaac56aa7400 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js @@ -99,7 +99,7 @@ function buildRequest(req, config, esIndexPattern) { 'aggregations.by_follower_index.buckets.by_shard_id.buckets.follower_lag_ops.value', ], body: { - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], query: { bool: { must: [ diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js index 4ee6cfe7fc54f..0a4b60b173254 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js @@ -37,7 +37,7 @@ async function getCcrStat(req, esIndexPattern, filters) { 'hits.hits.inner_hits.oldest.hits.hits._source.ccr_stats.failed_read_requests', ], body: { - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], query: { bool: { must: [ diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts index 0585ec2c08274..d153c40bbe58b 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts @@ -355,7 +355,7 @@ async function fetchBeatsByType( }), from: page * HITS_SIZE, collapse: { field: `${type}.beat.uuid` }, - sort: [{ [`${type}.timestamp`]: 'desc' }], + sort: [{ [`${type}.timestamp`]: { order: 'desc', unmapped_type: 'long' } }], size: HITS_SIZE, }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index 708bef31d8ac8..6325ed0c4b052 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -64,7 +64,7 @@ export function fetchElasticsearchStats( }, }, collapse: { field: 'cluster_uuid' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index 0f6a86af79e45..481afc86fd115 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -329,7 +329,7 @@ export async function fetchHighLevelStats< // a more ideal field would be the concatenation of the uuid + transport address for duped UUIDs (copied installations) field: `${product}_stats.${product}.uuid`, }, - sort: [{ timestamp: 'desc' }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index 0d41ac0f46814..a8b68929e84b8 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -59,7 +59,7 @@ export function fetchLicenses( }, }, collapse: { field: 'cluster_uuid' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index 0e35fbb008bee..e8bd229265e37 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -5,7 +5,6 @@ */ import { - EuiBetaBadge, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -58,12 +57,7 @@ export function Header({

{i18n.translate('xpack.observability.home.title', { defaultMessage: 'Observability', - })}{' '} - + })}

diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 625ae94c90aa2..86466baa45410 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -70,13 +70,13 @@ function NewsItem({ item }: { item: INewsItem }) {
- - + + {i18n.translate('xpack.observability.news.readFullStory', { defaultMessage: 'Read full story', })} - - + +
diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx index 13f7159ba6043..b5bfe3eec7d35 100644 --- a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { useContext } from 'react'; -import { ThemeContext } from 'styled-components'; +import { useTheme } from './use_theme'; export function useChartTheme() { - const theme = useContext(ThemeContext); + const theme = useTheme(); return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; } diff --git a/x-pack/plugins/observability/public/hooks/use_theme.tsx b/x-pack/plugins/observability/public/hooks/use_theme.tsx new file mode 100644 index 0000000000000..d0449a4432d93 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_theme.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { EuiTheme } from '../../../../legacy/common/eui_styled_components'; + +export function useTheme() { + const theme: EuiTheme = useContext(ThemeContext); + return theme; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 03939736b64ae..0aecea59ad013 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -26,3 +26,6 @@ export { } from './hooks/use_track_metric'; export * from './typings'; + +export { useChartTheme } from './hooks/use_chart_theme'; +export { useTheme } from './hooks/use_theme'; diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 8870bcbc9fa38..10bbdaaae34a8 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -200,7 +200,7 @@ export function OverviewPage({ routeParams }: Props) { {!!newsFeed?.items?.length && ( - + )} diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index e5bca43cef562..79449e5d229b8 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -23,7 +23,7 @@ export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; export const CSV_FORMULA_CHARS = ['=', '+', '-', '@']; -export const WHITELISTED_JOB_CONTENT_TYPES = [ +export const ALLOWED_JOB_CONTENT_TYPES = [ 'application/json', 'application/pdf', CONTENT_TYPE_CSV, @@ -34,7 +34,7 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ // See: // https://github.com/chromium/chromium/blob/3611052c055897e5ebbc5b73ea295092e0c20141/services/network/public/cpp/header_util_unittest.cc#L50 // For a list of headers that chromium doesn't like -export const KBN_SCREENSHOT_HEADER_BLACKLIST = [ +export const KBN_SCREENSHOT_HEADER_BLOCK_LIST = [ 'accept-encoding', 'connection', 'content-length', @@ -51,7 +51,7 @@ export const KBN_SCREENSHOT_HEADER_BLACKLIST = [ 'keep-alive', ]; -export const KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN = ['proxy-']; +export const KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN = ['proxy-']; export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; diff --git a/x-pack/plugins/reporting/server/browsers/network_policy.test.ts b/x-pack/plugins/reporting/server/browsers/network_policy.test.ts index 5c7c74807162c..b2fb6af41b01c 100644 --- a/x-pack/plugins/reporting/server/browsers/network_policy.test.ts +++ b/x-pack/plugins/reporting/server/browsers/network_policy.test.ts @@ -109,28 +109,28 @@ describe('Network Policy', () => { expect(allowRequest(url, rules)).toEqual(false); }); - it('allows requests when hosts are whitelisted IP addresses', () => { + it('allows requests when hosts are allowed IP addresses', () => { const url = 'http://192.168.1.1/cool/route/bro'; const rules = [{ allow: true, host: '192.168.1.1' }, { allow: false }]; expect(allowRequest(url, rules)).toEqual(true); }); - it('denies requests when hosts are blacklisted IP addresses', () => { + it('denies requests when hosts are from blocked IP addresses', () => { const url = 'http://192.168.1.1/cool/route/bro'; const rules = [{ allow: false, host: '192.168.1.1' }, { allow: true }]; expect(allowRequest(url, rules)).toEqual(false); }); - it('allows requests when hosts are IP addresses not blacklisted', () => { + it('allows requests when hosts are IP addresses that are not blocked', () => { const url = 'http://192.168.2.1/cool/route/bro'; const rules = [{ allow: false, host: '192.168.1.1' }, { allow: true }]; expect(allowRequest(url, rules)).toEqual(true); }); - it('denies requests when hosts are IP addresses not whitelisted', () => { + it('denies requests when hosts are IP addresses not explicitly allowed', () => { const url = 'http://192.168.2.1/cool/route/bro'; const rules = [{ allow: true, host: '192.168.1.1' }, { allow: false }]; @@ -138,7 +138,7 @@ describe('Network Policy', () => { }); describe('Common cases', () => { - it('allows whitelisting of certain routes based upon protocol', () => { + it('allows certain routes based upon protocol', () => { const rules = [ { allow: true, host: 'kibana.com', protocol: 'http:' }, { allow: true, protocol: 'https:' }, @@ -150,7 +150,7 @@ describe('Network Policy', () => { expect(allowRequest('http://bad.com/some/route', rules)).toEqual(false); }); - it('allows blacklisting of certain IPs', () => { + it('allows blocking of certain IPs', () => { const rules = [{ allow: false, host: '169.254.0.0' }, { allow: true }]; expect(allowRequest('http://kibana.com/some/route', rules)).toEqual(true); @@ -158,7 +158,7 @@ describe('Network Policy', () => { expect(allowRequest('https://169.254.0.0/some/route', rules)).toEqual(false); }); - it('allows whitelisting a single host on https', () => { + it('allows single host on https', () => { const rules = [{ allow: true, host: 'kibana.com', protocol: 'https:' }, { allow: false }]; expect(allowRequest('http://kibana.com/some/route', rules)).toEqual(false); @@ -166,7 +166,7 @@ describe('Network Policy', () => { expect(allowRequest('https://kibana.com/some/route', rules)).toEqual(true); }); - it('allows whitelisting a single protocol to http', () => { + it('allows single protocol to http', () => { const rules = [{ allow: true, protocol: 'https:' }, { allow: false }]; expect(allowRequest('http://kibana.com/some/route', rules)).toEqual(false); @@ -174,7 +174,7 @@ describe('Network Policy', () => { expect(allowRequest('https://good.com/some/route', rules)).toEqual(true); }); - it('allows whitelisting a single domain', () => { + it('allows single domain', () => { const rules = [{ allow: true, host: 'kibana.com' }, { allow: false }]; expect(allowRequest('http://kibana.com/some/route', rules)).toEqual(true); @@ -182,7 +182,7 @@ describe('Network Policy', () => { expect(allowRequest('https://www-kibana.com/some/route', rules)).toEqual(false); }); - it('can blacklist bad protocols', () => { + it('can ban bad protocols', () => { const rules = [ { allow: true, protocol: 'http:' }, { allow: true, protocol: 'https:' }, diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index a4e114d6b2f2e..e0d03eb4864ca 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -8,5 +8,5 @@ export { decryptJobHeaders } from './decrypt_job_headers'; export { getConditionalHeaders } from './get_conditional_headers'; export { getCustomLogo } from './get_custom_logo'; export { getFullUrls } from './get_full_urls'; -export { omitBlacklistedHeaders } from './omit_blacklisted_headers'; +export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts similarity index 80% rename from x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts index abf5784dacff9..f40651603db8f 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { omitBlacklistedHeaders } from './index'; +import { omitBlockedHeaders } from './index'; -test(`omits blacklisted headers`, async () => { +test(`omits blocked headers`, async () => { const permittedHeaders = { foo: 'bar', baz: 'quix', }; - const blacklistedHeaders = { + const blockedHeaders = { 'accept-encoding': '', connection: 'upgrade', 'content-length': '', @@ -24,7 +24,7 @@ test(`omits blacklisted headers`, async () => { trailer: 's are for trucks', }; - const filteredHeaders = await omitBlacklistedHeaders({ + const filteredHeaders = await omitBlockedHeaders({ job: { title: 'cool-job-bro', type: 'csv', @@ -36,7 +36,7 @@ test(`omits blacklisted headers`, async () => { }, decryptedHeaders: { ...permittedHeaders, - ...blacklistedHeaders, + ...blockedHeaders, }, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts similarity index 69% rename from x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts index b2e0ce23aa3a5..946f033b4b481 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts @@ -5,11 +5,11 @@ */ import { omitBy } from 'lodash'; import { - KBN_SCREENSHOT_HEADER_BLACKLIST, - KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, + KBN_SCREENSHOT_HEADER_BLOCK_LIST, + KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN, } from '../../../common/constants'; -export const omitBlacklistedHeaders = ({ +export const omitBlockedHeaders = ({ job, decryptedHeaders, }: { @@ -20,8 +20,8 @@ export const omitBlacklistedHeaders = ({ decryptedHeaders, (_value, header: string) => header && - (KBN_SCREENSHOT_HEADER_BLACKLIST.includes(header) || - KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN.some((pattern) => + (KBN_SCREENSHOT_HEADER_BLOCK_LIST.includes(header) || + KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN.some((pattern) => header?.startsWith(pattern) )) ); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 96b896b938962..67fc51bbfc352 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -13,7 +13,7 @@ import { decryptJobHeaders, getConditionalHeaders, getFullUrls, - omitBlacklistedHeaders, + omitBlockedHeaders, } from '../../common'; import { generatePngObservableFactory } from '../lib/generate_png'; import { TaskPayloadPNG } from '../types'; @@ -37,7 +37,7 @@ export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFac const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), + map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })), map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), mergeMap((conditionalHeaders) => { const urls = getFullUrls({ config, job }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 78808400c9c9a..7d1bb109f508b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -14,7 +14,7 @@ import { getConditionalHeaders, getCustomLogo, getFullUrls, - omitBlacklistedHeaders, + omitBlockedHeaders, } from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; @@ -40,7 +40,7 @@ export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFac const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlacklistedHeaders({ job, decryptedHeaders })), + map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })), map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), mergeMap((conditionalHeaders) => getCustomLogo({ reporting, config, job, conditionalHeaders }) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 7e07779b5fd37..979283f9f037c 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { omitBlacklistedHeaders } from '../../export_types/common'; +import { omitBlockedHeaders } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; import { LevelLogger as Logger } from '../../lib'; @@ -65,7 +65,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log }; const headers = { - headers: omitBlacklistedHeaders({ + headers: omitBlockedHeaders({ job: null, decryptedHeaders, }), diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index aec1eee0eee14..6905146a6d3c1 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -6,7 +6,7 @@ import { kibanaResponseFactory } from 'kibana/server'; import { ReportingCore } from '../../'; -import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; +import { ALLOWED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { ReportingUser } from '../../types'; import { getDocumentPayloadFactory } from './get_document_payload'; import { jobsQueryFactory } from './jobs_query'; @@ -48,7 +48,7 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { const payload = getDocumentPayload(doc); - if (!payload.contentType || !WHITELISTED_JOB_CONTENT_TYPES.includes(payload.contentType)) { + if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { return res.badRequest({ body: `Unsupported content-type of ${payload.contentType} specified by job output`, }); diff --git a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx index e58d8e8421547..b677f6a1fe8bb 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -115,8 +115,8 @@ describe('', () => { wrapper.update(); }); - expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(1); - expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(1); + expect(wrapper.find('EuiFieldPassword[data-test-subj="currentPassword"]')).toHaveLength(1); + expect(wrapper.find('EuiFieldPassword[data-test-subj="newPassword"]')).toHaveLength(1); }); it(`does not display change password form for users in the saml realm`, async () => { diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap index 08f8034538ac3..99780542b97f4 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap @@ -166,6 +166,7 @@ exports[`LoginForm renders as expected 1`] = ` isLoading={false} name="password" onChange={[Function]} + type="dual" value="" /> diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index a929b50fa1ffa..901d43adb659d 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -221,6 +221,7 @@ export class LoginForm extends Component { id="password" name="password" data-test-subj="loginPassword" + type={'dual'} value={this.state.password} onChange={this.onPasswordChange} disabled={!this.isLoadingState(LoadingStateType.None)} diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index d41a05e00e53c..43eb32853cf63 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldText } from '@elastic/eui'; +import { EuiFieldPassword } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -14,15 +14,15 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { userAPIClientMock } from '../../index.mock'; function getCurrentPasswordField(wrapper: ReactWrapper) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); + return wrapper.find(EuiFieldPassword).filter('[data-test-subj="currentPassword"]'); } function getNewPasswordField(wrapper: ReactWrapper) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="newPassword"]'); + return wrapper.find(EuiFieldPassword).filter('[data-test-subj="newPassword"]'); } function getConfirmPasswordField(wrapper: ReactWrapper) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); + return wrapper.find(EuiFieldPassword).filter('[data-test-subj="confirmNewPassword"]'); } describe('', () => { diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 296a8f6c8693f..5d889abdf46ce 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiButtonEmpty, - EuiFieldText, + EuiFieldPassword, EuiFlexGroup, EuiFlexItem, EuiForm, @@ -72,10 +72,10 @@ export class ChangePasswordForm extends Component { /> } > - { /> } > - { /> } > - = { - isPartial: false, - isRunning: false, - rawResponse: { - took: 14, - timed_out: false, - _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, - hits: { total: -1, max_score: 0, hits: [] }, - aggregations: { - group_by_users: { - doc_count_error_upper_bound: -1, - sum_other_doc_count: 408, - buckets: [ - { - key: 'SYSTEM', - doc_count: 281, - failures: { - meta: {}, - doc_count: 0, - lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - successes: { - meta: {}, - doc_count: 4, - lastSuccess: { - hits: { - total: 4, - max_score: 0, - hits: [ - { - _index: 'winlogbeat-8.0.0-2020.09.02-000001', - _id: 'zqY7WXQBA6bGZw2uLeKI', - _score: null, - _source: { - process: { - name: 'services.exe', - pid: 564, - executable: 'C:\\Windows\\System32\\services.exe', - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - type: 'winlogbeat', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - version: '8.0.0', - user: { name: 'inside_winlogbeat_user' }, - }, - winlog: { - computer_name: 'siem-windows', - process: { pid: 576, thread: { id: 880 } }, - keywords: ['Audit Success'], - logon: { id: '0x3e7', type: 'Service' }, - channel: 'Security', - event_data: { - LogonGuid: '{00000000-0000-0000-0000-000000000000}', - TargetOutboundDomainName: '-', - VirtualAccount: '%%1843', - LogonType: '5', - IpPort: '-', - TransmittedServices: '-', - SubjectLogonId: '0x3e7', - LmPackageName: '-', - TargetOutboundUserName: '-', - KeyLength: '0', - TargetLogonId: '0x3e7', - RestrictedAdminMode: '-', - SubjectUserName: 'SIEM-WINDOWS$', - TargetLinkedLogonId: '0x0', - ElevatedToken: '%%1842', - SubjectDomainName: 'WORKGROUP', - IpAddress: '-', - ImpersonationLevel: '%%1833', - TargetUserName: 'SYSTEM', - LogonProcessName: 'Advapi ', - TargetDomainName: 'NT AUTHORITY', - SubjectUserSid: 'S-1-5-18', - TargetUserSid: 'S-1-5-18', - AuthenticationPackageName: 'Negotiate', - }, - opcode: 'Info', - version: 2, - record_id: 57818, - task: 'Logon', - event_id: 4624, - provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', - activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Security-Auditing', - }, - log: { level: 'information' }, - source: { domain: '-' }, - message: - 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', - cloud: { - availability_zone: 'us-central1-c', - instance: { name: 'siem-windows', id: '9156726559029788564' }, - provider: 'gcp', - machine: { type: 'g1-small' }, - project: { id: 'elastic-siem' }, - }, - '@timestamp': '2020-09-04T13:08:02.532Z', - related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, - ecs: { version: '1.5.0' }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 4624, - provider: 'Microsoft-Windows-Security-Auditing', - created: '2020-09-04T13:08:03.638Z', - kind: 'event', - module: 'security', - action: 'logged-in', - category: 'authentication', - type: 'start', - outcome: 'success', - }, - user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, - }, - sort: [1599224882532], - }, - ], - }, - }, - }, - }, - { - key: 'tsg', - doc_count: 1, - failures: { - doc_count: 0, - lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - successes: { - doc_count: 1, - lastSuccess: { - hits: { - total: 1, - max_score: 0, - hits: [ - { - _index: '.ds-logs-system.auth-default-000001', - _id: '9_sfWXQBc39KFIJbIsDh', - _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 20764 }, - log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, - source: { - geo: { - continent_name: 'Europe', - region_iso_code: 'DE-BE', - city_name: 'Berlin', - country_iso_code: 'DE', - region_name: 'Land Berlin', - location: { lon: 13.3512, lat: 52.5727 }, - }, - as: { number: 6805, organization: { name: 'Telefonica Germany' } }, - port: 57457, - ip: '77.183.42.188', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T11:49:21.000Z', - system: { - auth: { - ssh: { - method: 'publickey', - signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', - event: 'Accepted', - }, - }, - }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_success', - category: 'authentication', - dataset: 'system.auth', - outcome: 'success', - }, - user: { name: 'tsg' }, - }, - sort: [1599220161000], - }, - ], - }, - }, - }, - }, - { - key: 'admin', - doc_count: 23, - failures: { - doc_count: 23, - lastFailure: { - hits: { - total: 23, - max_score: 0, - hits: [ - { - _index: '.ds-logs-system.auth-default-000001', - _id: 'ZfxZWXQBc39KFIJbLN5U', - _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 22913 }, - log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'KR-28', - city_name: 'Incheon', - country_iso_code: 'KR', - region_name: 'Incheon', - location: { lon: 126.7288, lat: 37.4562 }, - }, - as: { number: 4766, organization: { name: 'Korea Telecom' } }, - ip: '59.15.3.197', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T13:40:46.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'admin' }, - }, - sort: [1599226846000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'user', - doc_count: 21, - failures: { - doc_count: 21, - lastFailure: { - hits: { - total: 21, - max_score: 0, - hits: [ - { - _index: 'filebeat-8.0.0-2020.09.02-000001', - _id: 'M_xLWXQBc39KFIJbY7Cb', - _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20671 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-NY', - city_name: 'New York', - country_iso_code: 'US', - region_name: 'New York', - location: { lon: -74, lat: 40.7157 }, - }, - ip: '64.227.88.245', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T13:25:43.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['64.227.88.245'], user: ['user'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T13:25:47.034172Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'user' }, - }, - sort: [1599225943000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'ubuntu', - doc_count: 18, - failures: { - doc_count: 18, - lastFailure: { - hits: { - total: 18, - max_score: 0, - hits: [ - { - _index: 'filebeat-8.0.0-2020.09.02-000001', - _id: 'nPxKWXQBc39KFIJb7q4w', - _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - type: 'filebeat', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20665 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-NY', - city_name: 'New York', - country_iso_code: 'US', - region_name: 'New York', - location: { lon: -74, lat: 40.7157 }, - }, - ip: '64.227.88.245', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T13:25:07.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T13:25:16.974606Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'ubuntu' }, - }, - sort: [1599225907000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'odoo', - doc_count: 17, - failures: { - doc_count: 17, - lastFailure: { - hits: { - total: 17, - max_score: 0, - hits: [ - { - _index: '.ds-logs-system.auth-default-000001', - _id: 'mPsfWXQBc39KFIJbI8HI', - _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - type: 'filebeat', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 21506 }, - log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-DL', - city_name: 'New Delhi', - country_iso_code: 'IN', - region_name: 'National Capital Territory of Delhi', - location: { lon: 77.2245, lat: 28.6358 }, - }, - as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, - ip: '180.151.228.166', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T12:26:36.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'odoo' }, - }, - sort: [1599222396000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'pi', - doc_count: 17, - failures: { - doc_count: 17, - lastFailure: { - hits: { - total: 17, - max_score: 0, - hits: [ - { - _index: 'filebeat-8.0.0-2020.09.02-000001', - _id: 'aaToWHQBA6bGZw2uR-St', - _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20475 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, - source: { - geo: { - continent_name: 'Europe', - region_iso_code: 'SE-AB', - city_name: 'Stockholm', - country_iso_code: 'SE', - region_name: 'Stockholm', - location: { lon: 17.7833, lat: 59.25 }, - }, - as: { number: 8473, organization: { name: 'Bahnhof AB' } }, - ip: '178.174.148.58', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T11:37:22.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['178.174.148.58'], user: ['pi'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T11:37:31.797423Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'pi' }, - }, - sort: [1599219442000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'demo', - doc_count: 14, - failures: { - doc_count: 14, - lastFailure: { - hits: { - total: 14, - max_score: 0, - hits: [ - { - _index: 'filebeat-8.0.0-2020.09.02-000001', - _id: 'VaP_V3QBA6bGZw2upUbg', - _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 19849 }, - log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, - source: { - geo: { - continent_name: 'Europe', - country_iso_code: 'HR', - location: { lon: 15.5, lat: 45.1667 }, - }, - as: { - number: 42864, - organization: { name: 'Giganet Internet Szolgaltato Kft' }, - }, - ip: '45.95.168.157', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T07:23:22.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['45.95.168.157'], user: ['demo'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T07:23:26.046346Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'demo' }, - }, - sort: [1599204202000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'git', - doc_count: 13, - failures: { - doc_count: 13, - lastFailure: { - hits: { - total: 13, - max_score: 0, - hits: [ - { - _index: '.ds-logs-system.auth-default-000001', - _id: 'PqYfWXQBA6bGZw2uIhVU', - _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 20396 }, - log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'CN-BJ', - city_name: 'Beijing', - country_iso_code: 'CN', - region_name: 'Beijing', - location: { lon: 116.3889, lat: 39.9288 }, - }, - as: { - number: 45090, - organization: { - name: 'Shenzhen Tencent Computer Systems Company Limited', - }, - }, - ip: '123.206.30.76', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T11:20:26.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'git' }, - }, - sort: [1599218426000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - { - key: 'webadmin', - doc_count: 13, - failures: { - doc_count: 13, - lastFailure: { - hits: { - total: 13, - max_score: 0, - hits: [ - { - _index: 'filebeat-8.0.0-2020.09.02-000001', - _id: 'iMABWHQBB-gskclyitP-', - _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 19870 }, - log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, - source: { - geo: { - continent_name: 'Europe', - country_iso_code: 'HR', - location: { lon: 15.5, lat: 45.1667 }, - }, - as: { - number: 42864, - organization: { name: 'Giganet Internet Szolgaltato Kft' }, - }, - ip: '45.95.168.157', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, - '@timestamp': '2020-09-04T07:25:28.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['45.95.168.157'], user: ['webadmin'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T07:25:30.236651Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'webadmin' }, - }, - sort: [1599204328000], - }, - ], - }, - }, - }, - successes: { - doc_count: 0, - lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, - }, - }, - ], - }, - user_count: { value: 188 }, - }, - }, - total: 21, - loaded: 21, -}; + sort: ({ + direction: Direction.desc, + field: 'success', + } as unknown) as SortField, + params: {}, + hostName: 'bastion00.siem.estc.dev', +} as HostDetailsRequestOptions; -export const formattedSearchStrategyResponse = { +export const mockSearchStrategyResponse: IEsSearchResponse = { isPartial: false, isRunning: false, rawResponse: { @@ -2132,12 +1291,12 @@ export const formattedSearchStrategyResponse = { }, total: 21, loaded: 21, +}; + +export const formattedSearchStrategyResponse = { inspect: { dsl: [ - '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "host_architecture": {\n "terms": {\n "field": "host.architecture",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_id": {\n "terms": {\n "field": "host.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_ip": {\n "terms": {\n "field": "host.ip",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_mac": {\n "terms": {\n "field": "host.mac",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_name": {\n "terms": {\n "field": "host.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_family": {\n "terms": {\n "field": "host.os.family",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_name": {\n "terms": {\n "field": "host.os.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_platform": {\n "terms": {\n "field": "host.os.platform",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_version": {\n "terms": {\n "field": "host.os.version",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_instance_id": {\n "terms": {\n "field": "cloud.instance.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_machine_type": {\n "terms": {\n "field": "cloud.machine.type",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_provider": {\n "terms": {\n "field": "cloud.provider",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_region": {\n "terms": {\n "field": "cloud.region",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n {\n "term": {\n "host.name": "bastion00"\n }\n },\n {\n "range": {\n "@timestamp": {\n "format": "strict_date_optional_time",\n "gte": "2020-09-02T15:17:13.678Z",\n "lte": "2020-09-03T15:17:13.678Z"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', - ], - response: [ - '{\n "isPartial": false,\n "isRunning": false,\n "rawResponse": {\n "took": 14,\n "timed_out": false,\n "_shards": {\n "total": 21,\n "successful": 21,\n "skipped": 0,\n "failed": 0\n },\n "hits": {\n "total": -1,\n "max_score": 0,\n "hits": []\n },\n "aggregations": {\n "group_by_users": {\n "doc_count_error_upper_bound": -1,\n "sum_other_doc_count": 408,\n "buckets": [\n {\n "key": "SYSTEM",\n "doc_count": 281,\n "failures": {\n "meta": {},\n "doc_count": 0,\n "lastFailure": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n },\n "successes": {\n "meta": {},\n "doc_count": 4,\n "lastSuccess": {\n "hits": {\n "total": 4,\n "max_score": 0,\n "hits": [\n {\n "_index": "winlogbeat-8.0.0-2020.09.02-000001",\n "_id": "zqY7WXQBA6bGZw2uLeKI",\n "_score": null,\n "_source": {\n "process": {\n "name": "services.exe",\n "pid": 564,\n "executable": "C:\\\\Windows\\\\System32\\\\services.exe"\n },\n "agent": {\n "build_date": "2020-07-16 09:16:27 +0000 UTC ",\n "name": "siem-windows",\n "commit": "4dcbde39492bdc3843034bba8db811c68cb44b97 ",\n "id": "05e1bff7-d7a8-416a-8554-aa10288fa07d",\n "type": "winlogbeat",\n "ephemeral_id": "655abd6c-6c33-435d-a2eb-79b2a01e6d61",\n "version": "8.0.0",\n "user": {\n "name": "inside_winlogbeat_user"\n }\n },\n "winlog": {\n "computer_name": "siem-windows",\n "process": {\n "pid": 576,\n "thread": {\n "id": 880\n }\n },\n "keywords": [\n "Audit Success"\n ],\n "logon": {\n "id": "0x3e7",\n "type": "Service"\n },\n "channel": "Security",\n "event_data": {\n "LogonGuid": "{00000000-0000-0000-0000-000000000000}",\n "TargetOutboundDomainName": "-",\n "VirtualAccount": "%%1843",\n "LogonType": "5",\n "IpPort": "-",\n "TransmittedServices": "-",\n "SubjectLogonId": "0x3e7",\n "LmPackageName": "-",\n "TargetOutboundUserName": "-",\n "KeyLength": "0",\n "TargetLogonId": "0x3e7",\n "RestrictedAdminMode": "-",\n "SubjectUserName": "SIEM-WINDOWS$",\n "TargetLinkedLogonId": "0x0",\n "ElevatedToken": "%%1842",\n "SubjectDomainName": "WORKGROUP",\n "IpAddress": "-",\n "ImpersonationLevel": "%%1833",\n "TargetUserName": "SYSTEM",\n "LogonProcessName": "Advapi ",\n "TargetDomainName": "NT AUTHORITY",\n "SubjectUserSid": "S-1-5-18",\n "TargetUserSid": "S-1-5-18",\n "AuthenticationPackageName": "Negotiate"\n },\n "opcode": "Info",\n "version": 2,\n "record_id": 57818,\n "task": "Logon",\n "event_id": 4624,\n "provider_guid": "{54849625-5478-4994-a5ba-3e3b0328c30d}",\n "activity_id": "{d2485217-6bac-0000-8fbb-3f7e2571d601}",\n "api": "wineventlog",\n "provider_name": "Microsoft-Windows-Security-Auditing"\n },\n "log": {\n "level": "information"\n },\n "source": {\n "domain": "-"\n },\n "message": "An account was successfully logged on.\\n\\nSubject:\\n\\tSecurity ID:\\t\\tS-1-5-18\\n\\tAccount Name:\\t\\tSIEM-WINDOWS$\\n\\tAccount Domain:\\t\\tWORKGROUP\\n\\tLogon ID:\\t\\t0x3E7\\n\\nLogon Information:\\n\\tLogon Type:\\t\\t5\\n\\tRestricted Admin Mode:\\t-\\n\\tVirtual Account:\\t\\tNo\\n\\tElevated Token:\\t\\tYes\\n\\nImpersonation Level:\\t\\tImpersonation\\n\\nNew Logon:\\n\\tSecurity ID:\\t\\tS-1-5-18\\n\\tAccount Name:\\t\\tSYSTEM\\n\\tAccount Domain:\\t\\tNT AUTHORITY\\n\\tLogon ID:\\t\\t0x3E7\\n\\tLinked Logon ID:\\t\\t0x0\\n\\tNetwork Account Name:\\t-\\n\\tNetwork Account Domain:\\t-\\n\\tLogon GUID:\\t\\t{00000000-0000-0000-0000-000000000000}\\n\\nProcess Information:\\n\\tProcess ID:\\t\\t0x234\\n\\tProcess Name:\\t\\tC:\\\\Windows\\\\System32\\\\services.exe\\n\\nNetwork Information:\\n\\tWorkstation Name:\\t-\\n\\tSource Network Address:\\t-\\n\\tSource Port:\\t\\t-\\n\\nDetailed Authentication Information:\\n\\tLogon Process:\\t\\tAdvapi \\n\\tAuthentication Package:\\tNegotiate\\n\\tTransited Services:\\t-\\n\\tPackage Name (NTLM only):\\t-\\n\\tKey Length:\\t\\t0\\n\\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\\n\\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\\n\\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\\n\\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\\n\\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\\n\\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\\n\\nThe authentication information fields provide detailed information about this specific logon request.\\n\\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\\n\\t- Transited services indicate which intermediate services have participated in this logon request.\\n\\t- Package name indicates which sub-protocol was used among the NTLM protocols.\\n\\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.",\n "cloud": {\n "availability_zone": "us-central1-c",\n "instance": {\n "name": "siem-windows",\n "id": "9156726559029788564"\n },\n "provider": "gcp",\n "machine": {\n "type": "g1-small"\n },\n "project": {\n "id": "elastic-siem"\n }\n },\n "@timestamp": "2020-09-04T13:08:02.532Z",\n "related": {\n "user": [\n "SYSTEM",\n "SIEM-WINDOWS$"\n ]\n },\n "ecs": {\n "version": "1.5.0"\n },\n "host": {\n "hostname": "siem-windows",\n "os": {\n "build": "17763.1397",\n "kernel": "10.0.17763.1397 (WinBuild.160101.0800)",\n "name": "Windows Server 2019 Datacenter",\n "family": "windows",\n "version": "10.0",\n "platform": "windows"\n },\n "ip": [\n "fe80::ecf5:decc:3ec3:767e",\n "10.200.0.15"\n ],\n "name": "siem-windows",\n "id": "ce1d3c9b-a815-4643-9641-ada0f2c00609",\n "mac": [\n "42:01:0a:c8:00:0f"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "code": 4624,\n "provider": "Microsoft-Windows-Security-Auditing",\n "created": "2020-09-04T13:08:03.638Z",\n "kind": "event",\n "module": "security",\n "action": "logged-in",\n "category": "authentication",\n "type": "start",\n "outcome": "success"\n },\n "user": {\n "domain": "NT AUTHORITY",\n "name": "SYSTEM",\n "id": "S-1-5-18"\n }\n },\n "sort": [\n 1599224882532\n ]\n }\n ]\n }\n }\n }\n },\n {\n "key": "tsg",\n "doc_count": 1,\n "failures": {\n "doc_count": 0,\n "lastFailure": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n },\n "successes": {\n "doc_count": 1,\n "lastSuccess": {\n "hits": {\n "total": 1,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "9_sfWXQBc39KFIJbIsDh",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 20764\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 552463\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "region_iso_code": "DE-BE",\n "city_name": "Berlin",\n "country_iso_code": "DE",\n "region_name": "Land Berlin",\n "location": {\n "lon": 13.3512,\n "lat": 52.5727\n }\n },\n "as": {\n "number": 6805,\n "organization": {\n "name": "Telefonica Germany"\n }\n },\n "port": 57457,\n "ip": "77.183.42.188"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:49:21.000Z",\n "system": {\n "auth": {\n "ssh": {\n "method": "publickey",\n "signature": "RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8",\n "event": "Accepted"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_success",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "success"\n },\n "user": {\n "name": "tsg"\n }\n },\n "sort": [\n 1599220161000\n ]\n }\n ]\n }\n }\n }\n },\n {\n "key": "admin",\n "doc_count": 23,\n "failures": {\n "doc_count": 23,\n "lastFailure": {\n "hits": {\n "total": 23,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "ZfxZWXQBc39KFIJbLN5U",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 22913\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 562910\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "KR-28",\n "city_name": "Incheon",\n "country_iso_code": "KR",\n "region_name": "Incheon",\n "location": {\n "lon": 126.7288,\n "lat": 37.4562\n }\n },\n "as": {\n "number": 4766,\n "organization": {\n "name": "Korea Telecom"\n }\n },\n "ip": "59.15.3.197"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:40:46.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "admin"\n }\n },\n "sort": [\n 1599226846000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "user",\n "doc_count": 21,\n "failures": {\n "doc_count": 21,\n "lastFailure": {\n "hits": {\n "total": 21,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "M_xLWXQBc39KFIJbY7Cb",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20671\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1028103\n },\n "source": {\n "geo": {\n "continent_name": "North America",\n "region_iso_code": "US-NY",\n "city_name": "New York",\n "country_iso_code": "US",\n "region_name": "New York",\n "location": {\n "lon": -74,\n "lat": 40.7157\n }\n },\n "ip": "64.227.88.245"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:25:43.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "64.227.88.245"\n ],\n "user": [\n "user"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T13:25:47.034172Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "user"\n }\n },\n "sort": [\n 1599225943000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "ubuntu",\n "doc_count": 18,\n "failures": {\n "doc_count": 18,\n "lastFailure": {\n "hits": {\n "total": 18,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "nPxKWXQBc39KFIJb7q4w",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "type": "filebeat",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20665\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1027372\n },\n "source": {\n "geo": {\n "continent_name": "North America",\n "region_iso_code": "US-NY",\n "city_name": "New York",\n "country_iso_code": "US",\n "region_name": "New York",\n "location": {\n "lon": -74,\n "lat": 40.7157\n }\n },\n "ip": "64.227.88.245"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:25:07.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "64.227.88.245"\n ],\n "user": [\n "ubuntu"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T13:25:16.974606Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "ubuntu"\n }\n },\n "sort": [\n 1599225907000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "odoo",\n "doc_count": 17,\n "failures": {\n "doc_count": 17,\n "lastFailure": {\n "hits": {\n "total": 17,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "mPsfWXQBc39KFIJbI8HI",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "type": "filebeat",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 21506\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 556761\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "IN-DL",\n "city_name": "New Delhi",\n "country_iso_code": "IN",\n "region_name": "National Capital Territory of Delhi",\n "location": {\n "lon": 77.2245,\n "lat": 28.6358\n }\n },\n "as": {\n "number": 10029,\n "organization": {\n "name": "SHYAM SPECTRA PVT LTD"\n }\n },\n "ip": "180.151.228.166"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T12:26:36.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "odoo"\n }\n },\n "sort": [\n 1599222396000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "pi",\n "doc_count": 17,\n "failures": {\n "doc_count": 17,\n "lastFailure": {\n "hits": {\n "total": 17,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "aaToWHQBA6bGZw2uR-St",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20475\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1019218\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "region_iso_code": "SE-AB",\n "city_name": "Stockholm",\n "country_iso_code": "SE",\n "region_name": "Stockholm",\n "location": {\n "lon": 17.7833,\n "lat": 59.25\n }\n },\n "as": {\n "number": 8473,\n "organization": {\n "name": "Bahnhof AB"\n }\n },\n "ip": "178.174.148.58"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:37:22.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "178.174.148.58"\n ],\n "user": [\n "pi"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T11:37:31.797423Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "pi"\n }\n },\n "sort": [\n 1599219442000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "demo",\n "doc_count": 14,\n "failures": {\n "doc_count": 14,\n "lastFailure": {\n "hits": {\n "total": 14,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "VaP_V3QBA6bGZw2upUbg",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 19849\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 981036\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "country_iso_code": "HR",\n "location": {\n "lon": 15.5,\n "lat": 45.1667\n }\n },\n "as": {\n "number": 42864,\n "organization": {\n "name": "Giganet Internet Szolgaltato Kft"\n }\n },\n "ip": "45.95.168.157"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T07:23:22.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "45.95.168.157"\n ],\n "user": [\n "demo"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T07:23:26.046346Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "demo"\n }\n },\n "sort": [\n 1599204202000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "git",\n "doc_count": 13,\n "failures": {\n "doc_count": 13,\n "lastFailure": {\n "hits": {\n "total": 13,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "PqYfWXQBA6bGZw2uIhVU",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 20396\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 550795\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "CN-BJ",\n "city_name": "Beijing",\n "country_iso_code": "CN",\n "region_name": "Beijing",\n "location": {\n "lon": 116.3889,\n "lat": 39.9288\n }\n },\n "as": {\n "number": 45090,\n "organization": {\n "name": "Shenzhen Tencent Computer Systems Company Limited"\n }\n },\n "ip": "123.206.30.76"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:20:26.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "git"\n }\n },\n "sort": [\n 1599218426000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "webadmin",\n "doc_count": 13,\n "failures": {\n "doc_count": 13,\n "lastFailure": {\n "hits": {\n "total": 13,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "iMABWHQBB-gskclyitP-",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 19870\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 984133\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "country_iso_code": "HR",\n "location": {\n "lon": 15.5,\n "lat": 45.1667\n }\n },\n "as": {\n "number": 42864,\n "organization": {\n "name": "Giganet Internet Szolgaltato Kft"\n }\n },\n "ip": "45.95.168.157"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T07:25:28.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "45.95.168.157"\n ],\n "user": [\n "webadmin"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T07:25:30.236651Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "webadmin"\n }\n },\n "sort": [\n 1599204328000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n }\n ]\n },\n "user_count": {\n "value": 188\n }\n }\n },\n "total": 21,\n "loaded": 21\n}', + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "host_architecture": {\n "terms": {\n "field": "host.architecture",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_id": {\n "terms": {\n "field": "host.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_ip": {\n "terms": {\n "field": "host.ip",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_mac": {\n "terms": {\n "field": "host.mac",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_name": {\n "terms": {\n "field": "host.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_family": {\n "terms": {\n "field": "host.os.family",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_name": {\n "terms": {\n "field": "host.os.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_platform": {\n "terms": {\n "field": "host.os.platform",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_version": {\n "terms": {\n "field": "host.os.version",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_instance_id": {\n "terms": {\n "field": "cloud.instance.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_machine_type": {\n "terms": {\n "field": "cloud.machine.type",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_provider": {\n "terms": {\n "field": "cloud.provider",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_region": {\n "terms": {\n "field": "cloud.region",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n {\n "term": {\n "host.name": "bastion00.siem.estc.dev"\n }\n },\n {\n "range": {\n "@timestamp": {\n "format": "strict_date_optional_time",\n "gte": "2020-09-02T15:17:13.678Z",\n "lte": "2020-09-03T15:17:13.678Z"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', ], }, hostDetails: {}, @@ -2158,62 +1317,222 @@ export const expectedDsl = { body: { aggregations: { host_architecture: { - terms: { field: 'host.architecture', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.architecture', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_id: { - terms: { field: 'host.id', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.id', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_ip: { - terms: { field: 'host.ip', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.ip', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_mac: { - terms: { field: 'host.mac', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.mac', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_name: { - terms: { field: 'host.name', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.name', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_os_family: { - terms: { field: 'host.os.family', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.os.family', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_os_name: { - terms: { field: 'host.os.name', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.os.name', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_os_platform: { - terms: { field: 'host.os.platform', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.os.platform', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, host_os_version: { - terms: { field: 'host.os.version', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'host.os.version', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, cloud_instance_id: { - terms: { field: 'cloud.instance.id', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'cloud.instance.id', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, cloud_machine_type: { - terms: { field: 'cloud.machine.type', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'cloud.machine.type', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, cloud_provider: { - terms: { field: 'cloud.provider', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'cloud.provider', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, cloud_region: { - terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } }, - aggs: { timestamp: { max: { field: '@timestamp' } } }, + terms: { + field: 'cloud.region', + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, }, }, query: { bool: { filter: [ - { term: { 'host.name': 'bastion00' } }, + { + term: { + 'host.name': 'bastion00.siem.estc.dev', + }, + }, { range: { '@timestamp': { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts index 616e4ed0bac38..8913f72aad61e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts @@ -29,10 +29,8 @@ export const hostDetails: SecuritySolutionFactory = { const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildHostDetailsQuery(options))], - response: [inspectStringifyObject(response)], }; const formattedHostItem = formatHostItem(aggregations); - return { ...response, inspect, hostDetails: formattedHostItem }; }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts index eab1966434859..ad1fabd156fad 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { buildHostDetailsQuery as buildQuery } from './query.host_details.dsl'; +import { buildHostDetailsQuery } from './query.host_details.dsl'; import { mockOptions, expectedDsl } from './__mocks__/'; -describe('buildQuery', () => { +describe('buildHostDetailsQuery', () => { test('build query from options correctly', () => { - expect(buildQuery(mockOptions)).toEqual(expectedDsl); + expect(buildHostDetailsQuery(mockOptions)).toEqual(expectedDsl); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/__mocks__/index.ts new file mode 100644 index 0000000000000..fbe007622005c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/__mocks__/index.ts @@ -0,0 +1,377 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + NetworkDetailsRequestOptions, + NetworkQueries, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkDetailsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + docValueFields: [ + { field: '@timestamp', format: 'date_time' }, + { field: 'event.created', format: 'date_time' }, + { field: 'event.end', format: 'date_time' }, + { field: 'event.ingested', format: 'date_time' }, + { field: 'event.start', format: 'date_time' }, + { field: 'file.accessed', format: 'date_time' }, + { field: 'file.created', format: 'date_time' }, + { field: 'file.ctime', format: 'date_time' }, + { field: 'file.mtime', format: 'date_time' }, + { field: 'package.installed', format: 'date_time' }, + { field: 'process.parent.start', format: 'date_time' }, + { field: 'process.start', format: 'date_time' }, + { field: 'system.audit.host.boottime', format: 'date_time' }, + { field: 'system.audit.package.installtime', format: 'date_time' }, + { field: 'system.audit.user.password.last_changed', format: 'date_time' }, + { field: 'tls.client.not_after', format: 'date_time' }, + { field: 'tls.client.not_before', format: 'date_time' }, + { field: 'tls.server.not_after', format: 'date_time' }, + { field: 'tls.server.not_before', format: 'date_time' }, + { field: 'aws.cloudtrail.user_identity.session_context.creation_date', format: 'date_time' }, + { field: 'azure.auditlogs.properties.activity_datetime', format: 'date_time' }, + { field: 'azure.enqueued_time', format: 'date_time' }, + { field: 'azure.signinlogs.properties.created_at', format: 'date_time' }, + { field: 'cef.extensions.agentReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.deviceCustomDate1', format: 'date_time' }, + { field: 'cef.extensions.deviceCustomDate2', format: 'date_time' }, + { field: 'cef.extensions.deviceReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.endTime', format: 'date_time' }, + { field: 'cef.extensions.fileCreateTime', format: 'date_time' }, + { field: 'cef.extensions.fileModificationTime', format: 'date_time' }, + { field: 'cef.extensions.flexDate1', format: 'date_time' }, + { field: 'cef.extensions.managerReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.oldFileCreateTime', format: 'date_time' }, + { field: 'cef.extensions.oldFileModificationTime', format: 'date_time' }, + { field: 'cef.extensions.startTime', format: 'date_time' }, + { field: 'checkpoint.subs_exp', format: 'date_time' }, + { field: 'crowdstrike.event.EndTimestamp', format: 'date_time' }, + { field: 'crowdstrike.event.IncidentEndTime', format: 'date_time' }, + { field: 'crowdstrike.event.IncidentStartTime', format: 'date_time' }, + { field: 'crowdstrike.event.ProcessEndTime', format: 'date_time' }, + { field: 'crowdstrike.event.ProcessStartTime', format: 'date_time' }, + { field: 'crowdstrike.event.StartTimestamp', format: 'date_time' }, + { field: 'crowdstrike.event.Timestamp', format: 'date_time' }, + { field: 'crowdstrike.event.UTCTimestamp', format: 'date_time' }, + { field: 'crowdstrike.metadata.eventCreationTime', format: 'date_time' }, + { field: 'gsuite.admin.email.log_search_filter.end_date', format: 'date_time' }, + { field: 'gsuite.admin.email.log_search_filter.start_date', format: 'date_time' }, + { field: 'gsuite.admin.user.birthdate', format: 'date_time' }, + { field: 'kafka.block_timestamp', format: 'date_time' }, + { field: 'microsoft.defender_atp.lastUpdateTime', format: 'date_time' }, + { field: 'microsoft.defender_atp.resolvedTime', format: 'date_time' }, + { field: 'misp.campaign.first_seen', format: 'date_time' }, + { field: 'misp.campaign.last_seen', format: 'date_time' }, + { field: 'misp.intrusion_set.first_seen', format: 'date_time' }, + { field: 'misp.intrusion_set.last_seen', format: 'date_time' }, + { field: 'misp.observed_data.first_observed', format: 'date_time' }, + { field: 'misp.observed_data.last_observed', format: 'date_time' }, + { field: 'misp.report.published', format: 'date_time' }, + { field: 'misp.threat_indicator.valid_from', format: 'date_time' }, + { field: 'misp.threat_indicator.valid_until', format: 'date_time' }, + { field: 'netflow.collection_time_milliseconds', format: 'date_time' }, + { field: 'netflow.exporter.timestamp', format: 'date_time' }, + { field: 'netflow.flow_end_microseconds', format: 'date_time' }, + { field: 'netflow.flow_end_milliseconds', format: 'date_time' }, + { field: 'netflow.flow_end_nanoseconds', format: 'date_time' }, + { field: 'netflow.flow_end_seconds', format: 'date_time' }, + { field: 'netflow.flow_start_microseconds', format: 'date_time' }, + { field: 'netflow.flow_start_milliseconds', format: 'date_time' }, + { field: 'netflow.flow_start_nanoseconds', format: 'date_time' }, + { field: 'netflow.flow_start_seconds', format: 'date_time' }, + { field: 'netflow.max_export_seconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_microseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_milliseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_nanoseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_seconds', format: 'date_time' }, + { field: 'netflow.min_export_seconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_microseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_milliseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_nanoseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_seconds', format: 'date_time' }, + { field: 'netflow.monitoring_interval_end_milli_seconds', format: 'date_time' }, + { field: 'netflow.monitoring_interval_start_milli_seconds', format: 'date_time' }, + { field: 'netflow.observation_time_microseconds', format: 'date_time' }, + { field: 'netflow.observation_time_milliseconds', format: 'date_time' }, + { field: 'netflow.observation_time_nanoseconds', format: 'date_time' }, + { field: 'netflow.observation_time_seconds', format: 'date_time' }, + { field: 'netflow.system_init_time_milliseconds', format: 'date_time' }, + { field: 'rsa.internal.lc_ctime', format: 'date_time' }, + { field: 'rsa.internal.time', format: 'date_time' }, + { field: 'rsa.time.effective_time', format: 'date_time' }, + { field: 'rsa.time.endtime', format: 'date_time' }, + { field: 'rsa.time.event_queue_time', format: 'date_time' }, + { field: 'rsa.time.event_time', format: 'date_time' }, + { field: 'rsa.time.expire_time', format: 'date_time' }, + { field: 'rsa.time.recorded_time', format: 'date_time' }, + { field: 'rsa.time.stamp', format: 'date_time' }, + { field: 'rsa.time.starttime', format: 'date_time' }, + { field: 'sophos.xg.date', format: 'date_time' }, + { field: 'sophos.xg.eventtime', format: 'date_time' }, + { field: 'sophos.xg.start_time', format: 'date_time' }, + ], + factoryQueryType: NetworkQueries.details, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + ip: '35.196.65.164', +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 2620, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + host: { + doc_count: 0, + results: { hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] } }, + }, + destination: { + meta: {}, + doc_count: 5, + geo: { + meta: {}, + doc_count: 5, + results: { + hits: { + total: { value: 5, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1523631609876537', + _score: null, + _source: { + destination: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + }, + sort: [1599703212208], + }, + ], + }, + }, + }, + as: { + meta: {}, + doc_count: 5, + results: { + hits: { + total: { value: 5, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1523631609876537', + _score: null, + _source: { + destination: { as: { number: 15169, organization: { name: 'Google LLC' } } }, + }, + sort: [1599703212208], + }, + ], + }, + }, + }, + lastSeen: { value: 1599703212208, value_as_string: '2020-09-10T02:00:12.208Z' }, + firstSeen: { value: 1598802015355, value_as_string: '2020-08-30T15:40:15.355Z' }, + }, + source: { + meta: {}, + doc_count: 5, + geo: { + meta: {}, + doc_count: 5, + results: { + hits: { + total: { value: 5, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1523631486500511', + _score: null, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + }, + sort: [1599703214494], + }, + ], + }, + }, + }, + as: { + meta: {}, + doc_count: 5, + results: { + hits: { + total: { value: 5, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1523631486500511', + _score: null, + _source: { + source: { as: { number: 15169, organization: { name: 'Google LLC' } } }, + }, + sort: [1599703214494], + }, + ], + }, + }, + }, + lastSeen: { value: 1599703214494, value_as_string: '2020-09-10T02:00:14.494Z' }, + firstSeen: { value: 1598802015107, value_as_string: '2020-08-30T15:40:15.107Z' }, + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggs": {\n "source": {\n "filter": {\n "term": {\n "source.ip": "35.196.65.164"\n }\n },\n "aggs": {\n "firstSeen": {\n "min": {\n "field": "@timestamp"\n }\n },\n "lastSeen": {\n "max": {\n "field": "@timestamp"\n }\n },\n "as": {\n "filter": {\n "exists": {\n "field": "source.as"\n }\n },\n "aggs": {\n "results": {\n "top_hits": {\n "size": 1,\n "_source": [\n "source.as"\n ],\n "sort": [\n {\n "@timestamp": "desc"\n }\n ]\n }\n }\n }\n },\n "geo": {\n "filter": {\n "exists": {\n "field": "source.geo"\n }\n },\n "aggs": {\n "results": {\n "top_hits": {\n "size": 1,\n "_source": [\n "source.geo"\n ],\n "sort": [\n {\n "@timestamp": "desc"\n }\n ]\n }\n }\n }\n }\n }\n },\n "destination": {\n "filter": {\n "term": {\n "destination.ip": "35.196.65.164"\n }\n },\n "aggs": {\n "firstSeen": {\n "min": {\n "field": "@timestamp"\n }\n },\n "lastSeen": {\n "max": {\n "field": "@timestamp"\n }\n },\n "as": {\n "filter": {\n "exists": {\n "field": "destination.as"\n }\n },\n "aggs": {\n "results": {\n "top_hits": {\n "size": 1,\n "_source": [\n "destination.as"\n ],\n "sort": [\n {\n "@timestamp": "desc"\n }\n ]\n }\n }\n }\n },\n "geo": {\n "filter": {\n "exists": {\n "field": "destination.geo"\n }\n },\n "aggs": {\n "results": {\n "top_hits": {\n "size": 1,\n "_source": [\n "destination.geo"\n ],\n "sort": [\n {\n "@timestamp": "desc"\n }\n ]\n }\n }\n }\n }\n }\n },\n "host": {\n "filter": {\n "term": {\n "host.ip": "35.196.65.164"\n }\n },\n "aggs": {\n "results": {\n "top_hits": {\n "size": 1,\n "_source": [\n "host"\n ],\n "sort": [\n {\n "@timestamp": "desc"\n }\n ]\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "should": []\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + }, + networkDetails: { + source: { + firstSeen: '2020-08-30T15:40:15.107Z', + lastSeen: '2020-09-10T02:00:14.494Z', + autonomousSystem: { number: 15169, organization: { name: 'Google LLC' } }, + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + destination: { + firstSeen: '2020-08-30T15:40:15.355Z', + lastSeen: '2020-09-10T02:00:12.208Z', + autonomousSystem: { number: 15169, organization: { name: 'Google LLC' } }, + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + host: {}, + }, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggs: { + source: { + filter: { term: { 'source.ip': '35.196.65.164' } }, + aggs: { + firstSeen: { min: { field: '@timestamp' } }, + lastSeen: { max: { field: '@timestamp' } }, + as: { + filter: { exists: { field: 'source.as' } }, + aggs: { + results: { + top_hits: { size: 1, _source: ['source.as'], sort: [{ '@timestamp': 'desc' }] }, + }, + }, + }, + geo: { + filter: { exists: { field: 'source.geo' } }, + aggs: { + results: { + top_hits: { size: 1, _source: ['source.geo'], sort: [{ '@timestamp': 'desc' }] }, + }, + }, + }, + }, + }, + destination: { + filter: { term: { 'destination.ip': '35.196.65.164' } }, + aggs: { + firstSeen: { min: { field: '@timestamp' } }, + lastSeen: { max: { field: '@timestamp' } }, + as: { + filter: { exists: { field: 'destination.as' } }, + aggs: { + results: { + top_hits: { + size: 1, + _source: ['destination.as'], + sort: [{ '@timestamp': 'desc' }], + }, + }, + }, + }, + geo: { + filter: { exists: { field: 'destination.geo' } }, + aggs: { + results: { + top_hits: { + size: 1, + _source: ['destination.geo'], + sort: [{ '@timestamp': 'desc' }], + }, + }, + }, + }, + }, + }, + host: { + filter: { term: { 'host.ip': '35.196.65.164' } }, + aggs: { + results: { top_hits: { size: 1, _source: ['host'], sort: [{ '@timestamp': 'desc' }] } }, + }, + }, + }, + query: { bool: { should: [] } }, + size: 0, + track_total_hits: false, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.test.ts new file mode 100644 index 0000000000000..6f54097a76fe7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.details_network.dsl'; +import { networkDetails } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkDetails search strategy', () => { + const buildNetworkDetailsQuery = jest.spyOn(buildQuery, 'buildNetworkDetailsQuery'); + + afterEach(() => { + buildNetworkDetailsQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkDetails.buildDsl(mockOptions); + expect(buildNetworkDetailsQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkDetails.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.test.ts new file mode 100644 index 0000000000000..93dca8ee03978 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildNetworkDetailsQuery } from './query.details_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildNetworkDetailsQuery', () => { + test('build query from options correctly', () => { + expect(buildNetworkDetailsQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts new file mode 100644 index 0000000000000..f0605c5523fd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/__mocks__/index.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkDnsFields, + NetworkDnsRequestOptions, + NetworkQueries, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkDnsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.dns, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + isPtrIncluded: false, + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: NetworkDnsFields.uniqueDomains, direction: Direction.desc }, + timerange: { interval: '12h', from: '2020-09-13T09:00:43.249Z', to: '2020-09-14T09:00:43.249Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 28, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { max_score: 0, hits: [], total: 0 }, + aggregations: { + dns_count: { value: 2 }, + dns_name_query_count: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'google.com', + doc_count: 1, + unique_domains: { value: 1 }, + dns_bytes_in: { value: 0 }, + dns_bytes_out: { value: 0 }, + }, + { + key: 'google.internal', + doc_count: 1, + unique_domains: { value: 1 }, + dns_bytes_in: { value: 0 }, + dns_bytes_out: { value: 0 }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 28, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { max_score: 0, hits: [] }, + aggregations: { + dns_count: { value: 2 }, + dns_name_query_count: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'google.com', + doc_count: 1, + unique_domains: { value: 1 }, + dns_bytes_in: { value: 0 }, + dns_bytes_out: { value: 0 }, + }, + { + key: 'google.internal', + doc_count: 1, + unique_domains: { value: 1 }, + dns_bytes_in: { value: 0 }, + dns_bytes_out: { value: 0 }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, + edges: [ + { + node: { + _id: 'google.com', + dnsBytesIn: 0, + dnsBytesOut: 0, + dnsName: 'google.com', + queryCount: 1, + uniqueDomains: 1, + }, + cursor: { value: 'google.com', tiebreaker: null }, + }, + { + node: { + _id: 'google.internal', + dnsBytesIn: 0, + dnsBytesOut: 0, + dnsName: 'google.internal', + queryCount: 1, + uniqueDomains: 1, + }, + cursor: { value: 'google.internal', tiebreaker: null }, + }, + ], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "dns_count": {\n "cardinality": {\n "field": "dns.question.registered_domain"\n }\n },\n "dns_name_query_count": {\n "terms": {\n "field": "dns.question.registered_domain",\n "size": 10,\n "order": {\n "unique_domains": "desc"\n }\n },\n "aggs": {\n "unique_domains": {\n "cardinality": {\n "field": "dns.question.name"\n }\n },\n "dns_bytes_in": {\n "sum": {\n "field": "source.bytes"\n }\n },\n "dns_bytes_out": {\n "sum": {\n "field": "destination.bytes"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T09:00:43.249Z",\n "lte": "2020-09-14T09:00:43.249Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ],\n "must_not": [\n {\n "term": {\n "dns.question.type": {\n "value": "PTR"\n }\n }\n }\n ]\n }\n }\n },\n "size": 0,\n "track_total_hits": false\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 2, showMorePagesIndicator: false }, + totalCount: 2, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + dns_count: { cardinality: { field: 'dns.question.registered_domain' } }, + dns_name_query_count: { + terms: { + field: 'dns.question.registered_domain', + size: 10, + order: { unique_domains: 'desc' }, + }, + aggs: { + unique_domains: { cardinality: { field: 'dns.question.name' } }, + dns_bytes_in: { sum: { field: 'source.bytes' } }, + dns_bytes_out: { sum: { field: 'destination.bytes' } }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-13T09:00:43.249Z', + lte: '2020-09-14T09:00:43.249Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + must_not: [{ term: { 'dns.question.type': { value: 'PTR' } } }], + }, + }, + }, + size: 0, + track_total_hits: false, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.test.ts new file mode 100644 index 0000000000000..22906a1ea138c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.dns_network.dsl'; +import { networkDns } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkDns search strategy', () => { + const mockBuildDnsQuery = jest.spyOn(buildQuery, 'buildDnsQuery'); + + afterEach(() => { + mockBuildDnsQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkDns.buildDsl(mockOptions); + expect(mockBuildDnsQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkDns.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.test.ts new file mode 100644 index 0000000000000..9e95990d13a44 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildDnsQuery } from './query.dns_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildDnsQuery', () => { + test('build query from options correctly', () => { + expect(buildDnsQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/__mocks__/index.ts new file mode 100644 index 0000000000000..9991d267707b7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/__mocks__/index.ts @@ -0,0 +1,670 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkDnsFields, + NetworkDnsRequestOptions, + NetworkQueries, + SortField, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkDnsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.http, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { direction: Direction.desc } as SortField, + timerange: { interval: '12h', from: '2020-09-13T09:00:43.249Z', to: '2020-09-14T09:00:43.249Z' }, +} as NetworkDnsRequestOptions; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 422, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { max_score: 0, hits: [], total: 0 }, + aggregations: { + http_count: { value: 1404 }, + url: { + doc_count_error_upper_bound: 1440, + sum_other_doc_count: 98077, + buckets: [ + { + key: '/_nodes?filter_path=nodes.*.version%2Cnodes.*.http.publish_address%2Cnodes.*.ip', + doc_count: 106704, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'GET', doc_count: 106704 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 68983 }, + { key: 'es.siem.estc.dev', doc_count: 37721 }, + ], + }, + source: { + hits: { + total: { value: 106704, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'L4wXh3QBc39KFIJbgXrN', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '67.173.227.94' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 200, doc_count: 72174 }, + { key: 401, doc_count: 34530 }, + ], + }, + }, + { + key: '/_bulk', + doc_count: 76744, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 76744 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 76737 }, + { key: 'es.siem.estc.dev', doc_count: 7 }, + ], + }, + source: { + hits: { + total: { value: 76744, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'tEIXh3QBB-gskclyiT2g', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '35.227.65.114' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 200, doc_count: 75394 }, + { key: 401, doc_count: 1350 }, + ], + }, + }, + { + key: '/.reporting-*/_search', + doc_count: 58746, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 58746 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 56305 }, + { key: 'es.siem.estc.dev', doc_count: 2441 }, + ], + }, + source: { + hits: { + total: { value: 58746, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'MYwXh3QBc39KFIJbgXrN', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '67.173.227.94' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 58746 }], + }, + }, + { + key: + '/.kibana-task-manager-xavier-m/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + doc_count: 28715, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 28715 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'es.siem.estc.dev:9200', doc_count: 28715 }], + }, + source: { + hits: { + total: { value: 28715, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'MIwXh3QBc39KFIJbgXrN', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '24.168.52.229' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 28715 }], + }, + }, + { + key: + '/.kibana-task-manager-andrewg-local-testing-7-9-ff/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + doc_count: 28161, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 28161 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'es.siem.estc.dev:9200', doc_count: 28161 }], + }, + source: { + hits: { + total: { value: 28161, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'MowXh3QBc39KFIJbgXrN', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '67.173.227.94' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 28161 }], + }, + }, + { + key: '/_security/user/_has_privileges', + doc_count: 23283, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 23283 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 21601 }, + { key: 'es.siem.estc.dev', doc_count: 1682 }, + ], + }, + source: { + hits: { + total: { value: 23283, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: '6Ywch3QBc39KFIJbVY_k', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '67.173.227.94' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 23283 }], + }, + }, + { + key: '/_xpack', + doc_count: 20724, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'GET', doc_count: 20724 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 17289 }, + { key: 'es.siem.estc.dev', doc_count: 3435 }, + ], + }, + source: { + hits: { + total: { value: 20724, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'rkIXh3QBB-gskclyiT2g', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '35.226.77.71' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 200, doc_count: 12084 }, + { key: 401, doc_count: 8640 }, + ], + }, + }, + { + key: '/', + doc_count: 18306, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'GET', doc_count: 18274 }, + { key: 'HEAD', doc_count: 29 }, + { key: 'POST', doc_count: 3 }, + ], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 37, + buckets: [ + { key: 'es.siem.estc.dev', doc_count: 8631 }, + { key: 'es.siem.estc.dev:9200', doc_count: 5757 }, + { key: 'es.siem.estc.dev:443', doc_count: 3858 }, + { key: '35.232.239.42', doc_count: 20 }, + ], + }, + source: { + hits: { + total: { value: 18306, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'JEIYh3QBB-gskclyYEfA', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '35.171.72.245' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 3, + buckets: [ + { key: 401, doc_count: 18220 }, + { key: 404, doc_count: 30 }, + { key: 302, doc_count: 27 }, + { key: 200, doc_count: 26 }, + ], + }, + }, + { + key: '/_monitoring/bulk?system_id=kibana&system_api_version=7&interval=10000ms', + doc_count: 18048, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'POST', doc_count: 18048 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'es.siem.estc.dev:9200', doc_count: 17279 }, + { key: 'es.siem.estc.dev', doc_count: 769 }, + ], + }, + source: { + hits: { + total: { value: 18048, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'sUIXh3QBB-gskclyiT2g', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '24.168.52.229' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 18048 }], + }, + }, + { + key: '/s/row-renderer-checking/api/reporting/jobs/count', + doc_count: 14046, + methods: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'GET', doc_count: 14046 }], + }, + domains: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'kibana.siem.estc.dev', doc_count: 14046 }], + }, + source: { + hits: { + total: { value: 14046, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 's0IXh3QBB-gskclyiT2g', + _score: 0, + _source: { + host: { name: 'bastion00.siem.estc.dev' }, + source: { ip: '75.134.244.183' }, + }, + }, + ], + }, + }, + status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 200, doc_count: 14046 }], + }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + edges: [ + { + node: { + _id: '/_nodes?filter_path=nodes.*.version%2Cnodes.*.http.publish_address%2Cnodes.*.ip', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['GET'], + statuses: ['200', '401'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '67.173.227.94', + path: '/_nodes?filter_path=nodes.*.version%2Cnodes.*.http.publish_address%2Cnodes.*.ip', + requestCount: 106704, + }, + cursor: { + value: '/_nodes?filter_path=nodes.*.version%2Cnodes.*.http.publish_address%2Cnodes.*.ip', + tiebreaker: null, + }, + }, + { + node: { + _id: '/_bulk', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['POST'], + statuses: ['200', '401'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '35.227.65.114', + path: '/_bulk', + requestCount: 76744, + }, + cursor: { value: '/_bulk', tiebreaker: null }, + }, + { + node: { + _id: '/.reporting-*/_search', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['POST'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '67.173.227.94', + path: '/.reporting-*/_search', + requestCount: 58746, + }, + cursor: { value: '/.reporting-*/_search', tiebreaker: null }, + }, + { + node: { + _id: + '/.kibana-task-manager-xavier-m/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + domains: ['es.siem.estc.dev:9200'], + methods: ['POST'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '24.168.52.229', + path: + '/.kibana-task-manager-xavier-m/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + requestCount: 28715, + }, + cursor: { + value: + '/.kibana-task-manager-xavier-m/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + tiebreaker: null, + }, + }, + { + node: { + _id: + '/.kibana-task-manager-andrewg-local-testing-7-9-ff/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + domains: ['es.siem.estc.dev:9200'], + methods: ['POST'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '67.173.227.94', + path: + '/.kibana-task-manager-andrewg-local-testing-7-9-ff/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + requestCount: 28161, + }, + cursor: { + value: + '/.kibana-task-manager-andrewg-local-testing-7-9-ff/_update_by_query?ignore_unavailable=true&refresh=true&max_docs=10&conflicts=proceed', + tiebreaker: null, + }, + }, + { + node: { + _id: '/_security/user/_has_privileges', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['POST'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '67.173.227.94', + path: '/_security/user/_has_privileges', + requestCount: 23283, + }, + cursor: { value: '/_security/user/_has_privileges', tiebreaker: null }, + }, + { + node: { + _id: '/_xpack', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['GET'], + statuses: ['200', '401'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '35.226.77.71', + path: '/_xpack', + requestCount: 20724, + }, + cursor: { value: '/_xpack', tiebreaker: null }, + }, + { + node: { + _id: '/', + domains: [ + 'es.siem.estc.dev', + 'es.siem.estc.dev:9200', + 'es.siem.estc.dev:443', + '35.232.239.42', + ], + methods: ['GET', 'HEAD', 'POST'], + statuses: ['401', '404', '302', '200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '35.171.72.245', + path: '/', + requestCount: 18306, + }, + cursor: { value: '/', tiebreaker: null }, + }, + { + node: { + _id: '/_monitoring/bulk?system_id=kibana&system_api_version=7&interval=10000ms', + domains: ['es.siem.estc.dev:9200', 'es.siem.estc.dev'], + methods: ['POST'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '24.168.52.229', + path: '/_monitoring/bulk?system_id=kibana&system_api_version=7&interval=10000ms', + requestCount: 18048, + }, + cursor: { + value: '/_monitoring/bulk?system_id=kibana&system_api_version=7&interval=10000ms', + tiebreaker: null, + }, + }, + { + node: { + _id: '/s/row-renderer-checking/api/reporting/jobs/count', + domains: ['kibana.siem.estc.dev'], + methods: ['GET'], + statuses: ['200'], + lastHost: 'bastion00.siem.estc.dev', + lastSourceIp: '75.134.244.183', + path: '/s/row-renderer-checking/api/reporting/jobs/count', + requestCount: 14046, + }, + cursor: { value: '/s/row-renderer-checking/api/reporting/jobs/count', tiebreaker: null }, + }, + ], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "http_count": {\n "cardinality": {\n "field": "url.path"\n }\n },\n "url": {\n "terms": {\n "field": "url.path",\n "size": 10,\n "order": {\n "_count": "desc"\n }\n },\n "aggs": {\n "methods": {\n "terms": {\n "field": "http.request.method",\n "size": 4\n }\n },\n "domains": {\n "terms": {\n "field": "url.domain",\n "size": 4\n }\n },\n "status": {\n "terms": {\n "field": "http.response.status_code",\n "size": 4\n }\n },\n "source": {\n "top_hits": {\n "size": 1,\n "_source": {\n "includes": [\n "host.name",\n "source.ip"\n ]\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T09:00:43.249Z",\n "lte": "2020-09-14T09:00:43.249Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "exists": {\n "field": "http.request.method"\n }\n }\n ]\n }\n }\n },\n "size": 0,\n "track_total_hits": false\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 50, showMorePagesIndicator: true }, + totalCount: 1404, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + http_count: { cardinality: { field: 'url.path' } }, + url: { + terms: { field: 'url.path', size: 10, order: { _count: 'desc' } }, + aggs: { + methods: { terms: { field: 'http.request.method', size: 4 } }, + domains: { terms: { field: 'url.domain', size: 4 } }, + status: { terms: { field: 'http.response.status_code', size: 4 } }, + source: { top_hits: { size: 1, _source: { includes: ['host.name', 'source.ip'] } } }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-13T09:00:43.249Z', + lte: '2020-09-14T09:00:43.249Z', + format: 'strict_date_optional_time', + }, + }, + }, + { exists: { field: 'http.request.method' } }, + ], + }, + }, + }, + size: 0, + track_total_hits: false, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.test.ts new file mode 100644 index 0000000000000..47946ee3ed20b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.http_network.dsl'; +import { networkHttp } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkHttp search strategy', () => { + const buildHttpQuery = jest.spyOn(buildQuery, 'buildHttpQuery'); + + afterEach(() => { + buildHttpQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkHttp.buildDsl(mockOptions); + expect(buildHttpQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkHttp.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.test.ts new file mode 100644 index 0000000000000..1d10b60374a2f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildHttpQuery } from './query.http_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildHttpQuery', () => { + test('build query from options correctly', () => { + expect(buildHttpQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts index feffe7f70afd9..dcffa60d8aa3c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts @@ -69,6 +69,7 @@ export const buildHttpQuery = ({ size: 0, track_total_hits: false, }; + return dslQuery; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.test.ts new file mode 100644 index 0000000000000..f901d9f3dab5d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { NetworkQueries } from '../../../../../common/search_strategy/security_solution'; + +import { networkFactory } from '.'; +import { networkDetails } from './details'; +import { networkDns } from './dns'; +import { networkHttp } from './http'; +import { networkOverview } from './overview'; +import { networkTls } from './tls'; +import { networkTopCountries } from './top_countries'; +import { networkTopNFlow } from './top_n_flow'; +import { networkUsers } from './users'; + +jest.mock('./details'); +jest.mock('./dns'); +jest.mock('./http'); +jest.mock('./overview'); +jest.mock('./tls'); +jest.mock('./top_countries'); +jest.mock('./top_n_flow'); +jest.mock('./users'); + +describe('networkFactory', () => { + test('should include correct apis', () => { + const expectedNetworkFactory = { + [NetworkQueries.details]: networkDetails, + [NetworkQueries.dns]: networkDns, + [NetworkQueries.http]: networkHttp, + [NetworkQueries.overview]: networkOverview, + [NetworkQueries.tls]: networkTls, + [NetworkQueries.topCountries]: networkTopCountries, + [NetworkQueries.topNFlow]: networkTopNFlow, + [NetworkQueries.users]: networkUsers, + }; + expect(networkFactory).toEqual(expectedNetworkFactory); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/__mocks__/index.ts new file mode 100644 index 0000000000000..8f34d31e49e1d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/__mocks__/index.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + NetworkOverviewRequestOptions, + NetworkQueries, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkOverviewRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.overview, + filterQuery: + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field":"source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + timerange: { interval: '12h', from: '2020-09-13T12:54:24.685Z', to: '2020-09-14T12:54:24.685Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 141, + timed_out: false, + _shards: { + total: 21, + successful: 21, + skipped: 0, + failed: 0, + }, + hits: { + total: 1349108, + max_score: 0, + hits: [], + }, + aggregations: { + unique_zeek_count: { + meta: {}, + doc_count: 0, + }, + unique_packetbeat_count: { + meta: {}, + doc_count: 0, + unique_tls_count: { + meta: {}, + doc_count: 0, + }, + }, + unique_filebeat_count: { + meta: {}, + doc_count: 1278559, + unique_netflow_count: { + doc_count: 0, + }, + unique_cisco_count: { + meta: {}, + doc_count: 0, + }, + unique_panw_count: { + meta: {}, + doc_count: 0, + }, + }, + unique_dns_count: { + meta: {}, + doc_count: 0, + }, + unique_flow_count: { + meta: {}, + doc_count: 0, + }, + unique_socket_count: { + doc_count: 0, + }, + unique_suricata_count: { + meta: {}, + doc_count: 0, + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "unique_flow_count": {\n "filter": {\n "term": {\n "type": "flow"\n }\n }\n },\n "unique_dns_count": {\n "filter": {\n "term": {\n "type": "dns"\n }\n }\n },\n "unique_suricata_count": {\n "filter": {\n "term": {\n "service.type": "suricata"\n }\n }\n },\n "unique_zeek_count": {\n "filter": {\n "term": {\n "service.type": "zeek"\n }\n }\n },\n "unique_socket_count": {\n "filter": {\n "term": {\n "event.dataset": "socket"\n }\n }\n },\n "unique_filebeat_count": {\n "filter": {\n "term": {\n "agent.type": "filebeat"\n }\n },\n "aggs": {\n "unique_netflow_count": {\n "filter": {\n "term": {\n "input.type": "netflow"\n }\n }\n },\n "unique_panw_count": {\n "filter": {\n "term": {\n "event.module": "panw"\n }\n }\n },\n "unique_cisco_count": {\n "filter": {\n "term": {\n "event.module": "cisco"\n }\n }\n }\n }\n },\n "unique_packetbeat_count": {\n "filter": {\n "term": {\n "agent.type": "packetbeat"\n }\n },\n "aggs": {\n "unique_tls_count": {\n "filter": {\n "term": {\n "network.protocol": "tls"\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T12:54:24.685Z",\n "lte": "2020-09-14T12:54:24.685Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + }, + overviewNetwork: { + auditbeatSocket: 0, + filebeatCisco: 0, + filebeatNetflow: 0, + filebeatPanw: 0, + filebeatSuricata: 0, + filebeatZeek: 0, + packetbeatDNS: 0, + packetbeatFlow: 0, + packetbeatTLS: 0, + }, +}; + +export const expectedDsl = { + allowNoIndices: true, + ignoreUnavailable: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + body: { + aggregations: { + unique_flow_count: { + filter: { + term: { + type: 'flow', + }, + }, + }, + unique_dns_count: { + filter: { + term: { + type: 'dns', + }, + }, + }, + unique_suricata_count: { + filter: { + term: { + 'service.type': 'suricata', + }, + }, + }, + unique_zeek_count: { + filter: { + term: { + 'service.type': 'zeek', + }, + }, + }, + unique_socket_count: { + filter: { + term: { + 'event.dataset': 'socket', + }, + }, + }, + unique_filebeat_count: { + filter: { + term: { + 'agent.type': 'filebeat', + }, + }, + aggs: { + unique_netflow_count: { + filter: { + term: { + 'input.type': 'netflow', + }, + }, + }, + unique_panw_count: { + filter: { + term: { + 'event.module': 'panw', + }, + }, + }, + unique_cisco_count: { + filter: { + term: { + 'event.module': 'cisco', + }, + }, + }, + }, + }, + unique_packetbeat_count: { + filter: { + term: { + 'agent.type': 'packetbeat', + }, + }, + aggs: { + unique_tls_count: { + filter: { + term: { + 'network.protocol': 'tls', + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.test.ts new file mode 100644 index 0000000000000..14b9fb3c71933 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.overview_network.dsl'; +import { networkOverview } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkOverview search strategy', () => { + const buildOverviewNetworkQuery = jest.spyOn(buildQuery, 'buildOverviewNetworkQuery'); + + afterEach(() => { + buildOverviewNetworkQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkOverview.buildDsl(mockOptions); + expect(buildOverviewNetworkQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkOverview.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/query.overview_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/query.overview_network.dsl.test.ts new file mode 100644 index 0000000000000..553df2ab80e58 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/query.overview_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildOverviewNetworkQuery } from './query.overview_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildOverviewNetworkQuery', () => { + test('build query from options correctly', () => { + expect(buildOverviewNetworkQuery(mockOptions)).toMatchObject(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts new file mode 100644 index 0000000000000..15e2ac3cc0f69 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkTlsFields, + NetworkTlsRequestOptions, + NetworkQueries, + FlowTargetSourceDest, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkTlsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.tls, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + flowTarget: FlowTargetSourceDest.source, + ip: '', + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: NetworkTlsFields._id, direction: Direction.desc }, + timerange: { interval: '12h', from: '2020-09-13T09:58:58.637Z', to: '2020-09-14T09:58:58.637Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 62, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + sha1: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + count: { value: 0 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + edges: [], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggs": {\n "count": {\n "cardinality": {\n "field": "tls.server.hash.sha1"\n }\n },\n "sha1": {\n "terms": {\n "field": "tls.server.hash.sha1",\n "size": 10,\n "order": {\n "_key": "desc"\n }\n },\n "aggs": {\n "issuers": {\n "terms": {\n "field": "tls.server.issuer"\n }\n },\n "subjects": {\n "terms": {\n "field": "tls.server.subject"\n }\n },\n "not_after": {\n "terms": {\n "field": "tls.server.not_after"\n }\n },\n "ja3": {\n "terms": {\n "field": "tls.server.ja3s"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T09:58:58.637Z",\n "lte": "2020-09-14T09:58:58.637Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 0, showMorePagesIndicator: false }, + totalCount: 0, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggs: { + count: { cardinality: { field: 'tls.server.hash.sha1' } }, + sha1: { + terms: { field: 'tls.server.hash.sha1', size: 10, order: { _key: 'desc' } }, + aggs: { + issuers: { terms: { field: 'tls.server.issuer' } }, + subjects: { terms: { field: 'tls.server.subject' } }, + not_after: { terms: { field: 'tls.server.not_after' } }, + ja3: { terms: { field: 'tls.server.ja3s' } }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-13T09:58:58.637Z', + lte: '2020-09-14T09:58:58.637Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.test.ts new file mode 100644 index 0000000000000..b8c2be5bc61f8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.tls_network.dsl'; +import { networkTls } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkTls search strategy', () => { + const buildNetworkTlsQuery = jest.spyOn(buildQuery, 'buildNetworkTlsQuery'); + + afterEach(() => { + buildNetworkTlsQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkTls.buildDsl(mockOptions); + expect(buildNetworkTlsQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkTls.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.test.ts new file mode 100644 index 0000000000000..39cfcc7ab462e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildNetworkTlsQuery } from './query.tls_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildNetworkTlsQuery', () => { + test('build query from options correctly', () => { + expect(buildNetworkTlsQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/__mocks__/index.ts new file mode 100644 index 0000000000000..0578d99accbf5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/__mocks__/index.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkTopCountriesRequestOptions, + NetworkQueries, + FlowTargetSourceDest, + NetworkTopTablesFields, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkTopCountriesRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.topCountries, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + flowTarget: FlowTargetSourceDest.destination, + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: NetworkTopTablesFields.bytes_in, direction: Direction.desc }, + timerange: { interval: '12h', from: '2020-09-13T09:58:58.637Z', to: '2020-09-14T09:58:58.637Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 62, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + sha1: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + count: { value: 0 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + edges: [], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "top_countries_count": {\n "cardinality": {\n "field": "destination.geo.country_iso_code"\n }\n },\n "destination": {\n "terms": {\n "field": "destination.geo.country_iso_code",\n "size": 10,\n "order": {\n "bytes_in": "desc"\n }\n },\n "aggs": {\n "bytes_in": {\n "sum": {\n "field": "source.bytes"\n }\n },\n "bytes_out": {\n "sum": {\n "field": "destination.bytes"\n }\n },\n "flows": {\n "cardinality": {\n "field": "network.community_id"\n }\n },\n "source_ips": {\n "cardinality": {\n "field": "source.ip"\n }\n },\n "destination_ips": {\n "cardinality": {\n "field": "destination.ip"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T09:58:58.637Z",\n "lte": "2020-09-14T09:58:58.637Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n }\n },\n "size": 0,\n "track_total_hits": false\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 0, showMorePagesIndicator: false }, + totalCount: 0, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + top_countries_count: { cardinality: { field: 'destination.geo.country_iso_code' } }, + destination: { + terms: { field: 'destination.geo.country_iso_code', size: 10, order: { bytes_in: 'desc' } }, + aggs: { + bytes_in: { sum: { field: 'source.bytes' } }, + bytes_out: { sum: { field: 'destination.bytes' } }, + flows: { cardinality: { field: 'network.community_id' } }, + source_ips: { cardinality: { field: 'source.ip' } }, + destination_ips: { cardinality: { field: 'destination.ip' } }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-13T09:58:58.637Z', + lte: '2020-09-14T09:58:58.637Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + size: 0, + track_total_hits: false, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.test.ts new file mode 100644 index 0000000000000..65d1306b6946e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.top_countries_network.dsl'; +import { networkTopCountries } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkTopCountries search strategy', () => { + const buildTopCountriesQuery = jest.spyOn(buildQuery, 'buildTopCountriesQuery'); + + afterEach(() => { + buildTopCountriesQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkTopCountries.buildDsl(mockOptions); + expect(buildTopCountriesQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkTopCountries.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts index 5b0ced06f2ee9..ba4565b068eb4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts @@ -43,7 +43,6 @@ export const networkTopCountries: SecuritySolutionFactory fakeTotalCount; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.test.ts new file mode 100644 index 0000000000000..cba2988aa240e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildTopCountriesQuery } from './query.top_countries_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildTopCountriesQuery', () => { + test('build query from options correctly', () => { + expect(buildTopCountriesQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts new file mode 100644 index 0000000000000..84c1ca129ecac --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts @@ -0,0 +1,847 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkTopNFlowRequestOptions, + NetworkQueries, + NetworkTopTablesFields, + FlowTargetSourceDest, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkTopNFlowRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.topNFlow, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + flowTarget: FlowTargetSourceDest.source, + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + timerange: { interval: '12h', from: '2020-09-13T10:16:46.870Z', to: '2020-09-14T10:16:46.870Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 191, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { max_score: 0, hits: [], total: 0 }, + aggregations: { + source: { + meta: {}, + doc_count_error_upper_bound: -1, + sum_other_doc_count: 500330, + buckets: [ + { + key: '10.142.0.7', + doc_count: 12116, + bytes_out: { value: 2581835370 }, + flows: { value: 1967 }, + bytes_in: { value: 0 }, + domain: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'siem-kibana', + doc_count: 3221, + timestamp: { value: 1600078221017, value_as_string: '2020-09-14T10:10:21.017Z' }, + }, + ], + }, + autonomous_system: { + meta: {}, + doc_count: 0, + top_as: { hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] } }, + }, + location: { + doc_count: 0, + top_geo: { hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] } }, + }, + destination_ips: { value: 264 }, + }, + { + key: '35.232.239.42', + doc_count: 2119, + bytes_out: { value: 86968388 }, + flows: { value: 922 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 2119, + top_as: { + hits: { + total: { value: 2119, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526378075029582', + _score: 0, + _source: { + source: { as: { number: 15169, organization: { name: 'Google LLC' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 2119, + top_geo: { + hits: { + total: { value: 2119, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526378075029582', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '151.101.200.204', + doc_count: 2, + bytes_out: { value: 1394839 }, + flows: { value: 2 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 2, + top_as: { + hits: { + total: { value: 2, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1527252060367158', + _score: 0, + _source: { + source: { as: { number: 54113, organization: { name: 'Fastly' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 2, + top_geo: { + hits: { + total: { value: 2, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1527252060367158', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + city_name: 'Ashburn', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.4728, lat: 39.0481 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '91.189.92.39', + doc_count: 1, + bytes_out: { value: 570550 }, + flows: { value: 1 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 1, + top_as: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526971840437636', + _score: 0, + _source: { + source: { + as: { number: 41231, organization: { name: 'Canonical Group Limited' } }, + }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 1, + top_geo: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526971840437636', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'GB-ENG', + city_name: 'London', + country_iso_code: 'GB', + region_name: 'England', + location: { lon: -0.0961, lat: 51.5132 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '10.142.0.5', + doc_count: 514, + bytes_out: { value: 565933 }, + flows: { value: 486 }, + bytes_in: { value: 0 }, + domain: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'infraops-docker-data', + doc_count: 514, + timestamp: { value: 1600078218215, value_as_string: '2020-09-14T10:10:18.215Z' }, + }, + ], + }, + autonomous_system: { + doc_count: 0, + top_as: { hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] } }, + }, + location: { + doc_count: 0, + top_geo: { hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] } }, + }, + destination_ips: { value: 343 }, + }, + { + key: '151.101.248.204', + doc_count: 6, + bytes_out: { value: 260903 }, + flows: { value: 6 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 6, + top_as: { + hits: { + total: { value: 6, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1527003062069535', + _score: 0, + _source: { + source: { as: { number: 54113, organization: { name: 'Fastly' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 6, + top_geo: { + hits: { + total: { value: 6, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1527003062069535', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + city_name: 'Ashburn', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.539, lat: 39.018 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '35.196.129.83', + doc_count: 1, + bytes_out: { value: 164079 }, + flows: { value: 1 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 1, + top_as: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526557113311472', + _score: 0, + _source: { + source: { as: { number: 15169, organization: { name: 'Google LLC' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 1, + top_geo: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526557113311472', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '151.101.2.217', + doc_count: 24, + bytes_out: { value: 158407 }, + flows: { value: 24 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 24, + top_as: { + hits: { + total: { value: 24, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526379128390241', + _score: 0, + _source: { + source: { as: { number: 54113, organization: { name: 'Fastly' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 24, + top_geo: { + hits: { + total: { value: 24, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526379128390241', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + country_iso_code: 'US', + location: { lon: -97.822, lat: 37.751 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '91.189.91.38', + doc_count: 1, + bytes_out: { value: 89031 }, + flows: { value: 1 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 1, + top_as: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526555996515551', + _score: 0, + _source: { + source: { + as: { number: 41231, organization: { name: 'Canonical Group Limited' } }, + }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 1, + top_geo: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526555996515551', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-MA', + city_name: 'Boston', + country_iso_code: 'US', + region_name: 'Massachusetts', + location: { lon: -71.0631, lat: 42.3562 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 1 }, + }, + { + key: '193.228.91.123', + doc_count: 33, + bytes_out: { value: 32170 }, + flows: { value: 33 }, + bytes_in: { value: 0 }, + domain: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + autonomous_system: { + doc_count: 33, + top_as: { + hits: { + total: { value: 33, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526584379144248', + _score: 0, + _source: { + source: { as: { number: 133766, organization: { name: 'YHSRV.LLC' } } }, + }, + }, + ], + }, + }, + }, + location: { + doc_count: 33, + top_geo: { + hits: { + total: { value: 33, relation: 'eq' }, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'dd4fa2d4bd-1526584379144248', + _score: 0, + _source: { + source: { + geo: { + continent_name: 'North America', + country_iso_code: 'US', + location: { lon: -97.822, lat: 37.751 }, + }, + }, + }, + }, + ], + }, + }, + }, + destination_ips: { value: 2 }, + }, + ], + }, + top_n_flow_count: { value: 738 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + edges: [ + { + node: { + _id: '10.142.0.7', + source: { + domain: ['siem-kibana'], + ip: '10.142.0.7', + location: null, + autonomous_system: null, + flows: 1967, + destination_ips: 264, + }, + network: { bytes_in: 0, bytes_out: 2581835370 }, + }, + cursor: { value: '10.142.0.7', tiebreaker: null }, + }, + { + node: { + _id: '35.232.239.42', + source: { + domain: [], + ip: '35.232.239.42', + location: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 15169, name: 'Google LLC' }, + flows: 922, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 86968388 }, + }, + cursor: { value: '35.232.239.42', tiebreaker: null }, + }, + { + node: { + _id: '151.101.200.204', + source: { + domain: [], + ip: '151.101.200.204', + location: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + city_name: 'Ashburn', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.4728, lat: 39.0481 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 54113, name: 'Fastly' }, + flows: 2, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 1394839 }, + }, + cursor: { value: '151.101.200.204', tiebreaker: null }, + }, + { + node: { + _id: '91.189.92.39', + source: { + domain: [], + ip: '91.189.92.39', + location: { + geo: { + continent_name: 'Europe', + region_iso_code: 'GB-ENG', + city_name: 'London', + country_iso_code: 'GB', + region_name: 'England', + location: { lon: -0.0961, lat: 51.5132 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 41231, name: 'Canonical Group Limited' }, + flows: 1, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 570550 }, + }, + cursor: { value: '91.189.92.39', tiebreaker: null }, + }, + { + node: { + _id: '10.142.0.5', + source: { + domain: ['infraops-docker-data'], + ip: '10.142.0.5', + location: null, + autonomous_system: null, + flows: 486, + destination_ips: 343, + }, + network: { bytes_in: 0, bytes_out: 565933 }, + }, + cursor: { value: '10.142.0.5', tiebreaker: null }, + }, + { + node: { + _id: '151.101.248.204', + source: { + domain: [], + ip: '151.101.248.204', + location: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + city_name: 'Ashburn', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.539, lat: 39.018 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 54113, name: 'Fastly' }, + flows: 6, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 260903 }, + }, + cursor: { value: '151.101.248.204', tiebreaker: null }, + }, + { + node: { + _id: '35.196.129.83', + source: { + domain: [], + ip: '35.196.129.83', + location: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-VA', + country_iso_code: 'US', + region_name: 'Virginia', + location: { lon: -77.2481, lat: 38.6583 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 15169, name: 'Google LLC' }, + flows: 1, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 164079 }, + }, + cursor: { value: '35.196.129.83', tiebreaker: null }, + }, + { + node: { + _id: '151.101.2.217', + source: { + domain: [], + ip: '151.101.2.217', + location: { + geo: { + continent_name: 'North America', + country_iso_code: 'US', + location: { lon: -97.822, lat: 37.751 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 54113, name: 'Fastly' }, + flows: 24, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 158407 }, + }, + cursor: { value: '151.101.2.217', tiebreaker: null }, + }, + { + node: { + _id: '91.189.91.38', + source: { + domain: [], + ip: '91.189.91.38', + location: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-MA', + city_name: 'Boston', + country_iso_code: 'US', + region_name: 'Massachusetts', + location: { lon: -71.0631, lat: 42.3562 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 41231, name: 'Canonical Group Limited' }, + flows: 1, + destination_ips: 1, + }, + network: { bytes_in: 0, bytes_out: 89031 }, + }, + cursor: { value: '91.189.91.38', tiebreaker: null }, + }, + { + node: { + _id: '193.228.91.123', + source: { + domain: [], + ip: '193.228.91.123', + location: { + geo: { + continent_name: 'North America', + country_iso_code: 'US', + location: { lon: -97.822, lat: 37.751 }, + }, + flowTarget: 'source', + }, + autonomous_system: { number: 133766, name: 'YHSRV.LLC' }, + flows: 33, + destination_ips: 2, + }, + network: { bytes_in: 0, bytes_out: 32170 }, + }, + cursor: { value: '193.228.91.123', tiebreaker: null }, + }, + ], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "top_n_flow_count": {\n "cardinality": {\n "field": "source.ip"\n }\n },\n "source": {\n "terms": {\n "field": "source.ip",\n "size": 10,\n "order": {\n "bytes_out": "desc"\n }\n },\n "aggs": {\n "bytes_in": {\n "sum": {\n "field": "destination.bytes"\n }\n },\n "bytes_out": {\n "sum": {\n "field": "source.bytes"\n }\n },\n "domain": {\n "terms": {\n "field": "source.domain",\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "location": {\n "filter": {\n "exists": {\n "field": "source.geo"\n }\n },\n "aggs": {\n "top_geo": {\n "top_hits": {\n "_source": "source.geo.*",\n "size": 1\n }\n }\n }\n },\n "autonomous_system": {\n "filter": {\n "exists": {\n "field": "source.as"\n }\n },\n "aggs": {\n "top_as": {\n "top_hits": {\n "_source": "source.as.*",\n "size": 1\n }\n }\n }\n },\n "flows": {\n "cardinality": {\n "field": "network.community_id"\n }\n },\n "destination_ips": {\n "cardinality": {\n "field": "destination.ip"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T10:16:46.870Z",\n "lte": "2020-09-14T10:16:46.870Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n }\n },\n "size": 0,\n "track_total_hits": false\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 50, showMorePagesIndicator: true }, + totalCount: 738, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + top_n_flow_count: { cardinality: { field: 'source.ip' } }, + source: { + terms: { field: 'source.ip', size: 10, order: { bytes_out: 'desc' } }, + aggs: { + bytes_in: { sum: { field: 'destination.bytes' } }, + bytes_out: { sum: { field: 'source.bytes' } }, + domain: { + terms: { field: 'source.domain', order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + location: { + filter: { exists: { field: 'source.geo' } }, + aggs: { top_geo: { top_hits: { _source: 'source.geo.*', size: 1 } } }, + }, + autonomous_system: { + filter: { exists: { field: 'source.as' } }, + aggs: { top_as: { top_hits: { _source: 'source.as.*', size: 1 } } }, + }, + flows: { cardinality: { field: 'network.community_id' } }, + destination_ips: { cardinality: { field: 'destination.ip' } }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-13T10:16:46.870Z', + lte: '2020-09-14T10:16:46.870Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + size: 0, + track_total_hits: false, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.test.ts new file mode 100644 index 0000000000000..fe3213a4e5388 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.top_n_flow_network.dsl'; +import { networkTopNFlow } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkTopNFlow search strategy', () => { + const buildTopNFlowQuery = jest.spyOn(buildQuery, 'buildTopNFlowQuery'); + + afterEach(() => { + buildTopNFlowQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkTopNFlow.buildDsl(mockOptions); + expect(buildTopNFlowQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkTopNFlow.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.test.ts new file mode 100644 index 0000000000000..902769ae9a30b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildTopNFlowQuery } from './query.top_n_flow_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildTopNFlowQuery', () => { + test('build query from options correctly', () => { + expect(buildTopNFlowQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/__mocks__/index.ts new file mode 100644 index 0000000000000..3f57de7c78d1a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/__mocks__/index.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + NetworkUsersRequestOptions, + NetworkQueries, + NetworkUsersFields, + FlowTarget, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: NetworkUsersRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: NetworkQueries.users, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + flowTarget: FlowTarget.source, + ip: '10.142.0.7', + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: NetworkUsersFields.name, direction: Direction.asc }, + timerange: { interval: '12h', from: '2020-09-13T10:16:46.870Z', to: '2020-09-14T10:16:46.870Z' }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 12, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + user_count: { value: 3 }, + users: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '_apt', + doc_count: 34, + groupName: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + groupId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '104', doc_count: 34 }], + }, + }, + { + key: 'root', + doc_count: 8852, + groupName: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + groupId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '0', doc_count: 8852 }], + }, + }, + { + key: 'tsg', + doc_count: 16, + groupName: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + groupId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '1005', doc_count: 16 }], + }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + ...mockSearchStrategyResponse, + edges: [ + { + node: { + _id: '_apt', + user: { id: ['104'], name: '_apt', groupId: [], groupName: [], count: 34 }, + }, + cursor: { value: '_apt', tiebreaker: null }, + }, + { + node: { + _id: 'root', + user: { id: ['0'], name: 'root', groupId: [], groupName: [], count: 8852 }, + }, + cursor: { value: 'root', tiebreaker: null }, + }, + { + node: { + _id: 'tsg', + user: { id: ['1005'], name: 'tsg', groupId: [], groupName: [], count: 16 }, + }, + cursor: { value: 'tsg', tiebreaker: null }, + }, + ], + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggs": {\n "user_count": {\n "cardinality": {\n "field": "user.name"\n }\n },\n "users": {\n "terms": {\n "field": "user.name",\n "size": 10,\n "order": {\n "_key": "asc"\n }\n },\n "aggs": {\n "id": {\n "terms": {\n "field": "user.id"\n }\n },\n "groupId": {\n "terms": {\n "field": "user.group.id"\n }\n },\n "groupName": {\n "terms": {\n "field": "user.group.name"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-13T10:16:46.870Z",\n "lte": "2020-09-14T10:16:46.870Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "term": {\n "source.ip": "10.142.0.7"\n }\n }\n ],\n "must_not": [\n {\n "term": {\n "event.category": "authentication"\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + }, + pageInfo: { activePage: 0, fakeTotalCount: 3, showMorePagesIndicator: false }, + totalCount: 3, +}; + +export const expectedDsl = { + allowNoIndices: true, + body: { + aggs: { + user_count: { cardinality: { field: 'user.name' } }, + users: { + aggs: { + groupId: { terms: { field: 'user.group.id' } }, + groupName: { terms: { field: 'user.group.name' } }, + id: { terms: { field: 'user.id' } }, + }, + terms: { field: 'user.name', order: { _key: 'asc' }, size: 10 }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-13T10:16:46.870Z', + lte: '2020-09-14T10:16:46.870Z', + }, + }, + }, + { term: { 'source.ip': '10.142.0.7' } }, + ], + must_not: [{ term: { 'event.category': 'authentication' } }], + }, + }, + size: 0, + track_total_hits: false, + }, + ignoreUnavailable: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.test.ts new file mode 100644 index 0000000000000..19ce687cd3f07 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getUsersEdges } from './helpers'; +import { mockSearchStrategyResponse, formattedSearchStrategyResponse } from './__mocks__'; + +describe('#getUsers', () => { + test('will format edges correctly', () => { + const edges = getUsersEdges(mockSearchStrategyResponse); + expect(edges).toEqual(formattedSearchStrategyResponse.edges); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.test.ts new file mode 100644 index 0000000000000..bd98ec0947b35 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { NetworkUsersRequestOptions } from '../../../../../../common/search_strategy/security_solution/network'; + +import * as buildQuery from './query.users_network.dsl'; +import { networkUsers } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('networkUsers search strategy', () => { + const buildUsersQuery = jest.spyOn(buildQuery, 'buildUsersQuery'); + + afterEach(() => { + buildUsersQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + networkUsers.buildDsl(mockOptions); + expect(buildUsersQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should throw error if query size is greater equal than DEFAULT_MAX_TABLE_QUERY_SIZE ', () => { + const overSizeOptions = { + ...mockOptions, + pagination: { + ...mockOptions.pagination, + querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, + }, + } as NetworkUsersRequestOptions; + + expect(() => { + networkUsers.buildDsl(overSizeOptions); + }).toThrowError(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await networkUsers.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.test.ts new file mode 100644 index 0000000000000..ce43e1fb49e9f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildUsersQuery } from './query.users_network.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildUsersQuery', () => { + test('build query from options correctly', () => { + expect(buildUsersQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index a02123c4a3f8d..45c41b4d1d69d 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -633,7 +633,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'aaa', + _id: 'task:aaa', _source: { type: 'task', task: { @@ -654,7 +654,104 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { + // this is invalid as it doesn't have the `type` prefix _id: 'bbb', + _source: { + type: 'task', + task: { + runAt, + taskType: 'bar', + schedule: { interval: '5m' }, + attempts: 2, + status: 'claiming', + params: '{ "shazm": 1 }', + state: '{ "henry": "The 8th" }', + user: 'dabo', + scope: ['reporting', 'ceo'], + ownerId: taskManagerId, + }, + }, + _seq_no: 3, + _primary_term: 4, + sort: ['b', 2], + }, + ]; + const { + result: { docs }, + args: { + search: { + body: { query }, + }, + }, + } = await testClaimAvailableTasks({ + opts: { + taskManagerId, + }, + claimingOpts: { + claimOwnershipUntil, + size: 10, + }, + hits: tasks, + }); + + expect(query.bool.must).toContainEqual({ + bool: { + must: [ + { + term: { + 'task.ownerId': taskManagerId, + }, + }, + { term: { 'task.status': 'claiming' } }, + ], + }, + }); + + expect(docs).toMatchObject([ + { + attempts: 0, + id: 'aaa', + schedule: undefined, + params: { hello: 'world' }, + runAt, + scope: ['reporting'], + state: { baby: 'Henhen' }, + status: 'claiming', + taskType: 'foo', + user: 'jimbo', + ownerId: taskManagerId, + }, + ]); + }); + + test('it filters out invalid tasks that arent SavedObjects', async () => { + const taskManagerId = uuid.v1(); + const claimOwnershipUntil = new Date(Date.now()); + const runAt = new Date(); + const tasks = [ + { + _id: 'task:aaa', + _source: { + type: 'task', + task: { + runAt, + taskType: 'foo', + schedule: undefined, + attempts: 0, + status: 'claiming', + params: '{ "hello": "world" }', + state: '{ "baby": "Henhen" }', + user: 'jimbo', + scope: ['reporting'], + ownerId: taskManagerId, + }, + }, + _seq_no: 1, + _primary_term: 2, + sort: ['a', 1], + }, + { + _id: 'task:bbb', _source: { type: 'task', task: { @@ -729,7 +826,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'aaa', + _id: 'task:aaa', _source: { type: 'task', task: { @@ -750,7 +847,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { - _id: 'bbb', + _id: 'task:bbb', _source: { type: 'task', task: { @@ -1069,7 +1166,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'claimed-by-id', + _id: 'task:claimed-by-id', _source: { type: 'task', task: { @@ -1093,7 +1190,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { - _id: 'claimed-by-schedule', + _id: 'task:claimed-by-schedule', _source: { type: 'task', task: { @@ -1117,7 +1214,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['b', 2], }, { - _id: 'already-running', + _id: 'task:already-running', _source: { type: 'task', task: { @@ -1378,8 +1475,8 @@ if (doc['task.runAt'].size()!=0) { }); function generateFakeTasks(count: number = 1) { - return _.times(count, () => ({ - _id: 'aaa', + return _.times(count, (index) => ({ + _id: `task:id-${index}`, _source: { type: 'task', task: {}, diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index f2da41053e6ab..acd19bd75f7a3 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -451,6 +451,7 @@ export class TaskStore { return { docs: (rawDocs as SavedObjectsRawDoc[]) + .filter((doc) => this.serializer.isRawSavedObject(doc)) .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), diff --git a/x-pack/plugins/transform/common/api_schemas/audit_messages.ts b/x-pack/plugins/transform/common/api_schemas/audit_messages.ts new file mode 100644 index 0000000000000..76e63af262674 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/audit_messages.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransformMessage } from '../types/messages'; + +export type GetTransformsAuditMessagesResponseSchema = TransformMessage[]; diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts new file mode 100644 index 0000000000000..80b14ce6adee8 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/common.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { TRANSFORM_STATE } from '../constants'; + +export const transformIdsSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + }) +); + +export type TransformIdsSchema = TypeOf; + +export const transformStateSchema = schema.oneOf([ + schema.literal(TRANSFORM_STATE.ABORTING), + schema.literal(TRANSFORM_STATE.FAILED), + schema.literal(TRANSFORM_STATE.INDEXING), + schema.literal(TRANSFORM_STATE.STARTED), + schema.literal(TRANSFORM_STATE.STOPPED), + schema.literal(TRANSFORM_STATE.STOPPING), +]); + +export const indexPatternTitleSchema = schema.object({ + /** Title of the index pattern for which to return stats. */ + indexPatternTitle: schema.string(), +}); + +export type IndexPatternTitleSchema = TypeOf; + +export const transformIdParamSchema = schema.object({ + transformId: schema.string(), +}); + +export type TransformIdParamSchema = TypeOf; + +export interface ResponseStatus { + success: boolean; + error?: any; +} + +export interface CommonResponseStatusSchema { + [key: string]: ResponseStatus; +} diff --git a/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts b/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts new file mode 100644 index 0000000000000..c4d1a1f5f7587 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { transformStateSchema, ResponseStatus } from './common'; + +export const deleteTransformsRequestSchema = schema.object({ + /** + * Delete Transform & Destination Index + */ + transformsInfo: schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) + ), + deleteDestIndex: schema.maybe(schema.boolean()), + deleteDestIndexPattern: schema.maybe(schema.boolean()), + forceDelete: schema.maybe(schema.boolean()), +}); + +export type DeleteTransformsRequestSchema = TypeOf; + +export interface DeleteTransformStatus { + transformDeleted: ResponseStatus; + destIndexDeleted?: ResponseStatus; + destIndexPatternDeleted?: ResponseStatus; + destinationIndex?: string | undefined; +} + +export interface DeleteTransformsResponseSchema { + [key: string]: DeleteTransformStatus; +} diff --git a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts new file mode 100644 index 0000000000000..3bdbb5f1ff702 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const fieldHistogramsRequestSchema = schema.object({ + /** Query to match documents in the index. */ + query: schema.any(), + /** The fields to return histogram data. */ + fields: schema.arrayOf(schema.any()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ + samplerShardSize: schema.number(), +}); + +export type FieldHistogramsRequestSchema = TypeOf; +export type FieldHistogramsResponseSchema = any[]; diff --git a/x-pack/plugins/transform/common/api_schemas/start_transforms.ts b/x-pack/plugins/transform/common/api_schemas/start_transforms.ts new file mode 100644 index 0000000000000..b9611636e61a8 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/start_transforms.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { transformIdsSchema, CommonResponseStatusSchema } from './common'; + +export const startTransformsRequestSchema = transformIdsSchema; +export type StartTransformsRequestSchema = TypeOf; +export type StartTransformsResponseSchema = CommonResponseStatusSchema; diff --git a/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts b/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts new file mode 100644 index 0000000000000..56956de20b49e --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { transformStateSchema, CommonResponseStatusSchema } from './common'; + +export const stopTransformsRequestSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) +); + +export type StopTransformsRequestSchema = TypeOf; +export type StopTransformsResponseSchema = CommonResponseStatusSchema; diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts new file mode 100644 index 0000000000000..155807a5c445f --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; + +import type { Dictionary } from '../types/common'; +import type { PivotAggDict } from '../types/pivot_aggs'; +import type { PivotGroupByDict } from '../types/pivot_group_by'; +import type { TransformId, TransformPivotConfig } from '../types/transform'; + +import { transformStateSchema } from './common'; + +// GET transforms +export const getTransformsRequestSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) +); + +export type GetTransformsRequestSchema = TypeOf; + +export interface GetTransformsResponseSchema { + count: number; + transforms: TransformPivotConfig[]; +} + +// schemas shared by parts of the preview, create and update endpoint +export const destSchema = schema.object({ + index: schema.string(), + pipeline: schema.maybe(schema.string()), +}); +export const pivotSchema = schema.object({ + group_by: schema.any(), + aggregations: schema.any(), +}); +export const settingsSchema = schema.object({ + max_page_search_size: schema.maybe(schema.number()), + // The default value is null, which disables throttling. + docs_per_second: schema.maybe(schema.nullable(schema.number())), +}); +export const sourceSchema = schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), +}); +export const syncSchema = schema.object({ + time: schema.object({ + delay: schema.maybe(schema.string()), + field: schema.string(), + }), +}); + +// PUT transforms/{transformId} +export const putTransformsRequestSchema = schema.object({ + description: schema.maybe(schema.string()), + dest: destSchema, + frequency: schema.maybe(schema.string()), + pivot: pivotSchema, + settings: schema.maybe(settingsSchema), + source: sourceSchema, + sync: schema.maybe(syncSchema), +}); + +export interface PutTransformsRequestSchema extends TypeOf { + pivot: { + group_by: PivotGroupByDict; + aggregations: PivotAggDict; + }; +} + +interface TransformCreated { + transform: TransformId; +} +interface TransformCreatedError { + id: TransformId; + error: any; +} +export interface PutTransformsResponseSchema { + transformsCreated: TransformCreated[]; + errors: TransformCreatedError[]; +} + +// POST transforms/_preview +export const postTransformsPreviewRequestSchema = schema.object({ + pivot: pivotSchema, + source: sourceSchema, +}); + +export interface PostTransformsPreviewRequestSchema + extends TypeOf { + pivot: { + group_by: PivotGroupByDict; + aggregations: PivotAggDict; + }; +} + +interface EsMappingType { + type: ES_FIELD_TYPES; +} + +export type PreviewItem = Dictionary; +export type PreviewData = PreviewItem[]; +export type PreviewMappingsProperties = Dictionary; + +export interface PostTransformsPreviewResponseSchema { + generated_dest_index: { + mappings: { + _meta: { + _transform: { + transform: string; + version: { create: string }; + creation_date_in_millis: number; + }; + created_by: string; + }; + properties: PreviewMappingsProperties; + }; + settings: { index: { number_of_shards: string; auto_expand_replicas: string } }; + aliases: Record; + }; + preview: PreviewData; +} diff --git a/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts new file mode 100644 index 0000000000000..30661a8a407da --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { TransformStats } from '../types/transform_stats'; + +import { getTransformsRequestSchema } from './transforms'; + +export const getTransformsStatsRequestSchema = getTransformsRequestSchema; + +export type GetTransformsRequestSchema = TypeOf; + +export interface GetTransformsStatsResponseSchema { + node_failures?: object; + count: number; + transforms: TransformStats[]; +} diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts new file mode 100644 index 0000000000000..f9753a412527e --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { SearchResponse7 } from '../../../ml/common'; + +import type { EsIndex } from '../types/es_index'; + +// To be able to use the type guards on the client side, we need to make sure we don't import +// the code of '@kbn/config-schema' but just its types, otherwise the client side code will +// fail to build. +import type { FieldHistogramsResponseSchema } from './field_histograms'; +import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages'; +import type { DeleteTransformsResponseSchema } from './delete_transforms'; +import type { StartTransformsResponseSchema } from './start_transforms'; +import type { StopTransformsResponseSchema } from './stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewResponseSchema, + PutTransformsResponseSchema, +} from './transforms'; +import type { GetTransformsStatsResponseSchema } from './transforms_stats'; +import type { PostTransformsUpdateResponseSchema } from './update_transforms'; + +const isBasicObject = (arg: any) => { + return typeof arg === 'object' && arg !== null; +}; + +const isGenericResponseSchema = (arg: any): arg is T => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'count') && + {}.hasOwnProperty.call(arg, 'transforms') && + Array.isArray(arg.transforms) + ); +}; + +export const isGetTransformsResponseSchema = (arg: any): arg is GetTransformsResponseSchema => { + return isGenericResponseSchema(arg); +}; + +export const isGetTransformsStatsResponseSchema = ( + arg: any +): arg is GetTransformsStatsResponseSchema => { + return isGenericResponseSchema(arg); +}; + +export const isDeleteTransformsResponseSchema = ( + arg: any +): arg is DeleteTransformsResponseSchema => { + return ( + isBasicObject(arg) && + Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'transformDeleted'))) + ); +}; + +export const isEsIndices = (arg: any): arg is EsIndex[] => { + return Array.isArray(arg); +}; + +export const isEsSearchResponse = (arg: any): arg is SearchResponse7 => { + return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'hits'); +}; + +export const isFieldHistogramsResponseSchema = (arg: any): arg is FieldHistogramsResponseSchema => { + return Array.isArray(arg); +}; + +export const isGetTransformsAuditMessagesResponseSchema = ( + arg: any +): arg is GetTransformsAuditMessagesResponseSchema => { + return Array.isArray(arg); +}; + +export const isPostTransformsPreviewResponseSchema = ( + arg: any +): arg is PostTransformsPreviewResponseSchema => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'generated_dest_index') && + {}.hasOwnProperty.call(arg, 'preview') && + typeof arg.generated_dest_index !== undefined && + Array.isArray(arg.preview) + ); +}; + +export const isPostTransformsUpdateResponseSchema = ( + arg: any +): arg is PostTransformsUpdateResponseSchema => { + return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'id') && typeof arg.id === 'string'; +}; + +export const isPutTransformsResponseSchema = (arg: any): arg is PutTransformsResponseSchema => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'transformsCreated') && + {}.hasOwnProperty.call(arg, 'errors') && + Array.isArray(arg.transformsCreated) && + Array.isArray(arg.errors) + ); +}; + +const isGenericSuccessResponseSchema = (arg: any) => + isBasicObject(arg) && Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'success'))); + +export const isStartTransformsResponseSchema = (arg: any): arg is StartTransformsResponseSchema => { + return isGenericSuccessResponseSchema(arg); +}; + +export const isStopTransformsResponseSchema = (arg: any): arg is StopTransformsResponseSchema => { + return isGenericSuccessResponseSchema(arg); +}; diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts new file mode 100644 index 0000000000000..e303d94ef0536 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { TransformPivotConfig } from '../types/transform'; + +import { destSchema, settingsSchema, sourceSchema, syncSchema } from './transforms'; + +// POST _transform/{transform_id}/_update +export const postTransformsUpdateRequestSchema = schema.object({ + description: schema.maybe(schema.string()), + dest: schema.maybe(destSchema), + frequency: schema.maybe(schema.string()), + settings: schema.maybe(settingsSchema), + source: schema.maybe(sourceSchema), + sync: schema.maybe(syncSchema), +}); + +export type PostTransformsUpdateRequestSchema = TypeOf; +export type PostTransformsUpdateResponseSchema = TransformPivotConfig; diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index b01a82dffa04a..5efb6f31c1e3f 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -75,3 +75,24 @@ export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [ ]; export const APP_INDEX_PRIVILEGES = ['monitor']; + +// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 +export const TRANSFORM_STATE = { + ABORTING: 'aborting', + FAILED: 'failed', + INDEXING: 'indexing', + STARTED: 'started', + STOPPED: 'stopped', + STOPPING: 'stopping', +} as const; + +const transformStates = Object.values(TRANSFORM_STATE); +export type TransformState = typeof transformStates[number]; + +export const TRANSFORM_MODE = { + BATCH: 'batch', + CONTINUOUS: 'continuous', +} as const; + +const transformModes = Object.values(TRANSFORM_MODE); +export type TransformMode = typeof transformModes[number]; diff --git a/x-pack/plugins/transform/common/index.ts b/x-pack/plugins/transform/common/index.ts deleted file mode 100644 index 08bb4022c7016..0000000000000 --- a/x-pack/plugins/transform/common/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} - -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} - -export type TransformId = string; - -// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 -export enum TRANSFORM_STATE { - ABORTING = 'aborting', - FAILED = 'failed', - INDEXING = 'indexing', - STARTED = 'started', - STOPPED = 'stopped', - STOPPING = 'stopping', -} - -export interface TransformEndpointRequest { - id: TransformId; - state?: TRANSFORM_STATE; -} - -export interface ResultData { - success: boolean; - error?: any; -} - -export interface TransformEndpointResult { - [key: string]: ResultData; -} - -export interface DeleteTransformEndpointRequest { - transformsInfo: TransformEndpointRequest[]; - deleteDestIndex?: boolean; - deleteDestIndexPattern?: boolean; - forceDelete?: boolean; -} - -export interface DeleteTransformStatus { - transformDeleted: ResultData; - destIndexDeleted?: ResultData; - destIndexPatternDeleted?: ResultData; - destinationIndex?: string | undefined; -} - -export interface DeleteTransformEndpointResult { - [key: string]: DeleteTransformStatus; -} diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts new file mode 100644 index 0000000000000..8681204755c36 --- /dev/null +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type { SearchResponse7 } from '../../ml/common'; diff --git a/x-pack/plugins/transform/common/types/aggregations.ts b/x-pack/plugins/transform/common/types/aggregations.ts new file mode 100644 index 0000000000000..77b7e55e3ba94 --- /dev/null +++ b/x-pack/plugins/transform/common/types/aggregations.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type AggName = string; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api_types.ts b/x-pack/plugins/transform/common/types/es_index.ts similarity index 100% rename from x-pack/plugins/transform/public/app/hooks/use_api_types.ts rename to x-pack/plugins/transform/common/types/es_index.ts diff --git a/x-pack/plugins/transform/common/types/fields.ts b/x-pack/plugins/transform/common/types/fields.ts new file mode 100644 index 0000000000000..2c274f3bd9b48 --- /dev/null +++ b/x-pack/plugins/transform/common/types/fields.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type EsFieldName = string; diff --git a/x-pack/plugins/transform/common/types/pivot_aggs.ts b/x-pack/plugins/transform/common/types/pivot_aggs.ts new file mode 100644 index 0000000000000..d50609da6a5dc --- /dev/null +++ b/x-pack/plugins/transform/common/types/pivot_aggs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AggName } from './aggregations'; +import { EsFieldName } from './fields'; + +export const PIVOT_SUPPORTED_AGGS = { + AVG: 'avg', + CARDINALITY: 'cardinality', + MAX: 'max', + MIN: 'min', + PERCENTILES: 'percentiles', + SUM: 'sum', + VALUE_COUNT: 'value_count', + FILTER: 'filter', +} as const; + +export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS]; + +export type PivotAgg = { + [key in PivotSupportedAggs]?: { + field: EsFieldName; + }; +}; + +export type PivotAggDict = { + [key in AggName]: PivotAgg; +}; diff --git a/x-pack/plugins/transform/common/types/pivot_group_by.ts b/x-pack/plugins/transform/common/types/pivot_group_by.ts new file mode 100644 index 0000000000000..bfaf17a32b580 --- /dev/null +++ b/x-pack/plugins/transform/common/types/pivot_group_by.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dictionary } from './common'; +import { EsFieldName } from './fields'; + +export type GenericAgg = object; + +export interface TermsAgg { + terms: { + field: EsFieldName; + }; +} + +export interface HistogramAgg { + histogram: { + field: EsFieldName; + interval: string; + }; +} + +export interface DateHistogramAgg { + date_histogram: { + field: EsFieldName; + calendar_interval: string; + }; +} + +export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg; +export type PivotGroupByDict = Dictionary; diff --git a/x-pack/plugins/transform/common/types/privileges.ts b/x-pack/plugins/transform/common/types/privileges.ts new file mode 100644 index 0000000000000..bf710b8225599 --- /dev/null +++ b/x-pack/plugins/transform/common/types/privileges.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface MissingPrivileges { + [key: string]: string[] | undefined; +} + +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts new file mode 100644 index 0000000000000..6b31705442706 --- /dev/null +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PutTransformsRequestSchema } from '../api_schemas/transforms'; + +export type IndexName = string; +export type IndexPattern = string; +export type TransformId = string; + +export interface TransformPivotConfig extends PutTransformsRequestSchema { + id: TransformId; + create_time?: number; + version?: string; +} diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts new file mode 100644 index 0000000000000..5bd2fd955845c --- /dev/null +++ b/x-pack/plugins/transform/common/types/transform_stats.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransformState, TRANSFORM_STATE } from '../constants'; +import { TransformId } from './transform'; + +export interface TransformStats { + id: TransformId; + checkpointing: { + last: { + checkpoint: number; + timestamp_millis?: number; + }; + next?: { + checkpoint: number; + checkpoint_progress?: { + total_docs: number; + docs_remaining: number; + percent_complete: number; + }; + }; + operations_behind: number; + }; + node?: { + id: string; + name: string; + ephemeral_id: string; + transport_address: string; + attributes: Record; + }; + stats: { + documents_indexed: number; + documents_processed: number; + index_failures: number; + index_time_in_ms: number; + index_total: number; + pages_processed: number; + search_failures: number; + search_time_in_ms: number; + search_total: number; + trigger_count: number; + processing_time_in_ms: number; + processing_total: number; + exponential_avg_checkpoint_duration_ms: number; + exponential_avg_documents_indexed: number; + exponential_avg_documents_processed: number; + }; + reason?: string; + state: TransformState; +} + +export function isTransformStats(arg: any): arg is TransformStats { + return ( + typeof arg === 'object' && + arg !== null && + {}.hasOwnProperty.call(arg, 'state') && + Object.values(TRANSFORM_STATE).includes(arg.state) + ); +} diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index f7441fd93f38a..470c42d5de7fa 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -23,7 +23,6 @@ export { DataGrid, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 397a58006f1d1..507579d374353 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -6,7 +6,7 @@ import { composeValidators, patternValidator } from '../../../../ml/public'; -export type AggName = string; +import { AggName } from '../../../common/types/aggregations'; export function isAggName(arg: any): arg is AggName { // allow all characters except `[]>` and must not start or end with a space. diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts index 0e5ecb5d3b214..6d96f614b28a4 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.test.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { - getPreviewRequestBody, + getPreviewTransformRequestBody, PivotAggsConfig, PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, SimpleQuery, } from '../common'; @@ -35,7 +36,12 @@ describe('Transform: Data Grid', () => { aggName: 'the-agg-agg-name', dropDownName: 'the-agg-drop-down-name', }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const request = getPreviewTransformRequestBody( + 'the-index-pattern-title', + query, + [groupBy], + [agg] + ); const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index cf9ba5d6f5853..08f834431fa8b 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import type { PostTransformsPreviewRequestSchema } from '../../../common/api_schemas/transforms'; + import { PivotQuery } from './request'; -import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { +export const getPivotPreviewDevConsoleStatement = (request: PostTransformsPreviewRequestSchema) => { return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/fields.ts b/x-pack/plugins/transform/public/app/common/fields.ts index b22aae255b9fa..778750e1f97e4 100644 --- a/x-pack/plugins/transform/public/app/common/fields.ts +++ b/x-pack/plugins/transform/public/app/common/fields.ts @@ -5,10 +5,10 @@ */ import { Dictionary } from '../../../common/types/common'; +import { EsFieldName } from '../../../common/types/fields'; export type EsId = string; export type EsDocSource = Dictionary; -export type EsFieldName = string; export interface EsDoc extends Dictionary { _id: EsId; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index 45ddc440057b2..0fc947eaf33b0 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AggName, isAggName } from './aggregations'; +export { isAggName } from './aggregations'; export { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement, @@ -17,44 +17,28 @@ export { toggleSelectedField, EsDoc, EsDocSource, - EsFieldName, } from './fields'; export { DropDownLabel, DropDownOption, Label } from './dropdown'; export { isTransformIdValid, refreshTransformList$, useRefreshTransformList, - CreateRequestBody, - PreviewRequestBody, - TransformPivotConfig, - IndexName, - IndexPattern, REFRESH_TRANSFORM_LIST_STATE, } from './transform'; export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list'; -export { - getTransformProgress, - isCompletedBatchTransform, - isTransformStats, - TransformStats, - TRANSFORM_MODE, -} from './transform_stats'; +export { getTransformProgress, isCompletedBatchTransform } from './transform_stats'; export { getDiscoverUrl } from './navigation'; -export { GetTransformsResponse, PreviewData, PreviewMappings } from './pivot_preview'; export { getEsAggFromAggConfig, isPivotAggsConfigWithUiSupport, isPivotAggsConfigPercentiles, PERCENTILES_AGG_DEFAULT_PERCENTS, - PivotAgg, - PivotAggDict, PivotAggsConfig, PivotAggsConfigDict, PivotAggsConfigBase, PivotAggsConfigWithUiSupport, PivotAggsConfigWithUiSupportDict, pivotAggsFieldSupport, - PIVOT_SUPPORTED_AGGS, } from './pivot_aggs'; export { dateHistogramIntervalFormatRegex, @@ -65,25 +49,19 @@ export { isGroupByHistogram, isGroupByTerms, pivotGroupByFieldSupport, - DateHistogramAgg, - GenericAgg, GroupByConfigWithInterval, GroupByConfigWithUiSupport, - HistogramAgg, - PivotGroupBy, PivotGroupByConfig, - PivotGroupByDict, PivotGroupByConfigDict, PivotGroupByConfigWithUiSupportDict, PivotSupportedGroupByAggs, PivotSupportedGroupByAggsWithInterval, PIVOT_SUPPORTED_GROUP_BY_AGGS, - TermsAgg, } from './pivot_group_by'; export { defaultQuery, - getPreviewRequestBody, - getCreateRequestBody, + getPreviewTransformRequestBody, + getCreateTransformRequestBody, getPivotQuery, isDefaultQuery, isMatchAllQuery, diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index ec52de4b9da92..7a7bb4c65b306 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,31 +5,22 @@ */ import { FC } from 'react'; -import { Dictionary } from '../../../common/types/common'; + import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { AggName } from './aggregations'; -import { EsFieldName } from './fields'; +import type { AggName } from '../../../common/types/aggregations'; +import type { Dictionary } from '../../../common/types/common'; +import type { EsFieldName } from '../../../common/types/fields'; +import type { PivotAgg, PivotSupportedAggs } from '../../../common/types/pivot_aggs'; +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { getAggFormConfig } from '../sections/create_transform/components/step_define/common/get_agg_form_config'; import { PivotAggsConfigFilter } from '../sections/create_transform/components/step_define/common/filter_agg/types'; -export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS]; - export function isPivotSupportedAggs(arg: any): arg is PivotSupportedAggs { return Object.values(PIVOT_SUPPORTED_AGGS).includes(arg); } -export const PIVOT_SUPPORTED_AGGS = { - AVG: 'avg', - CARDINALITY: 'cardinality', - MAX: 'max', - MIN: 'min', - PERCENTILES: 'percentiles', - SUM: 'sum', - VALUE_COUNT: 'value_count', - FILTER: 'filter', -} as const; - export const PERCENTILES_AGG_DEFAULT_PERCENTS = [1, 5, 25, 50, 75, 95, 99]; export const pivotAggsFieldSupport = { @@ -69,16 +60,6 @@ export const pivotAggsFieldSupport = { [KBN_FIELD_TYPES.CONFLICT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER], }; -export type PivotAgg = { - [key in PivotSupportedAggs]?: { - field: EsFieldName; - }; -}; - -export type PivotAggDict = { - [key in AggName]: PivotAgg; -}; - /** * The maximum level of sub-aggregations */ diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index 7da52fc018338..2c2bac369c72d 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AggName } from '../../../common/types/aggregations'; import { Dictionary } from '../../../common/types/common'; +import { EsFieldName } from '../../../common/types/fields'; +import { GenericAgg } from '../../../common/types/pivot_group_by'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { AggName } from './aggregations'; -import { EsFieldName } from './fields'; - export enum PIVOT_SUPPORTED_GROUP_BY_AGGS { DATE_HISTOGRAM = 'date_histogram', HISTOGRAM = 'histogram', @@ -106,31 +106,6 @@ export function isPivotGroupByConfigWithUiSupport(arg: any): arg is GroupByConfi return isGroupByDateHistogram(arg) || isGroupByHistogram(arg) || isGroupByTerms(arg); } -export type GenericAgg = object; - -export interface TermsAgg { - terms: { - field: EsFieldName; - }; -} - -export interface HistogramAgg { - histogram: { - field: EsFieldName; - interval: string; - }; -} - -export interface DateHistogramAgg { - date_histogram: { - field: EsFieldName; - calendar_interval: string; - }; -} - -export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg; -export type PivotGroupByDict = Dictionary; - export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): GenericAgg { const { agg, aggName, dropDownName, ...esAgg } = groupByConfig; diff --git a/x-pack/plugins/transform/public/app/common/pivot_preview.ts b/x-pack/plugins/transform/public/app/common/pivot_preview.ts deleted file mode 100644 index 14368a80b0131..0000000000000 --- a/x-pack/plugins/transform/public/app/common/pivot_preview.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../common/types/common'; - -interface EsMappingType { - type: ES_FIELD_TYPES; -} - -export type PreviewItem = Dictionary; -export type PreviewData = PreviewItem[]; -export interface PreviewMappings { - properties: Dictionary; -} - -export interface GetTransformsResponse { - preview: PreviewData; - generated_dest_index: { - mappings: PreviewMappings; - // Not in use yet - aliases: any; - settings: any; - }; -} diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 63f1f8b10ad44..416927c460842 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { PivotGroupByConfig } from '../common'; import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs'; +import { PivotAggsConfig } from './pivot_aggs'; import { defaultQuery, - getPreviewRequestBody, - getCreateRequestBody, + getPreviewTransformRequestBody, + getCreateTransformRequestBody, getPivotQuery, isDefaultQuery, isMatchAllQuery, @@ -55,7 +57,7 @@ describe('Transform: Common', () => { }); }); - test('getPreviewRequestBody()', () => { + test('getPreviewTransformRequestBody()', () => { const query = getPivotQuery('the-query'); const groupBy: PivotGroupByConfig[] = [ { @@ -73,7 +75,7 @@ describe('Transform: Common', () => { dropDownName: 'the-agg-drop-down-name', }, ]; - const request = getPreviewRequestBody('the-index-pattern-title', query, groupBy, aggs); + const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); expect(request).toEqual({ pivot: { @@ -87,7 +89,7 @@ describe('Transform: Common', () => { }); }); - test('getPreviewRequestBody() with comma-separated index pattern', () => { + test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { const query = getPivotQuery('the-query'); const groupBy: PivotGroupByConfig[] = [ { @@ -105,7 +107,7 @@ describe('Transform: Common', () => { dropDownName: 'the-agg-drop-down-name', }, ]; - const request = getPreviewRequestBody( + const request = getPreviewTransformRequestBody( 'the-index-pattern-title,the-other-title', query, groupBy, @@ -124,7 +126,7 @@ describe('Transform: Common', () => { }); }); - test('getCreateRequestBody()', () => { + test('getCreateTransformRequestBody()', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', @@ -160,7 +162,7 @@ describe('Transform: Common', () => { valid: true, }; - const request = getCreateRequestBody( + const request = getCreateTransformRequestBody( 'the-index-pattern-title', pivotState, transformDetailsState diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 9a0084c2ebffb..10f3a63477029 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -4,15 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DefaultOperator } from 'elasticsearch'; - +import type { DefaultOperator } from 'elasticsearch'; + +import { HttpFetchError } from '../../../../../../src/core/public'; +import type { IndexPattern } from '../../../../../../src/plugins/data/public'; + +import type { + PostTransformsPreviewRequestSchema, + PutTransformsRequestSchema, +} from '../../../common/api_schemas/transforms'; +import type { + DateHistogramAgg, + HistogramAgg, + TermsAgg, +} from '../../../common/types/pivot_group_by'; import { dictionaryToArray } from '../../../common/types/common'; -import { SavedSearchQuery } from '../hooks/use_search_items'; - -import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; -import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; -import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import type { SavedSearchQuery } from '../hooks/use_search_items'; +import type { StepDefineExposedState } from '../sections/create_transform/components/step_define'; +import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { getEsAggFromAggConfig, @@ -24,8 +34,6 @@ import { } from '../common'; import { PivotAggsConfig } from './pivot_aggs'; -import { DateHistogramAgg, HistogramAgg, TermsAgg } from './pivot_group_by'; -import { PreviewRequestBody, CreateRequestBody } from './transform'; export interface SimpleQuery { query_string: { @@ -63,17 +71,18 @@ export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } -export function getPreviewRequestBody( +export function getPreviewTransformRequestBody( indexPatternTitle: IndexPattern['title'], query: PivotQuery, groupBy: PivotGroupByConfig[], aggs: PivotAggsConfig[] -): PreviewRequestBody { +): PostTransformsPreviewRequestSchema { const index = indexPatternTitle.split(',').map((name: string) => name.trim()); - const request: PreviewRequestBody = { + const request: PostTransformsPreviewRequestSchema = { source: { index, + ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), }, pivot: { group_by: {}, @@ -81,10 +90,6 @@ export function getPreviewRequestBody( }, }; - if (!isDefaultQuery(query) && !isMatchAllQuery(query)) { - request.source.query = query; - } - groupBy.forEach((g) => { if (isGroupByTerms(g)) { const termsAgg: TermsAgg = { @@ -125,37 +130,41 @@ export function getPreviewRequestBody( return request; } -export function getCreateRequestBody( +export const getCreateTransformRequestBody = ( indexPatternTitle: IndexPattern['title'], pivotState: StepDefineExposedState, transformDetailsState: StepDetailsExposedState -): CreateRequestBody { - const request: CreateRequestBody = { - ...getPreviewRequestBody( - indexPatternTitle, - getPivotQuery(pivotState.searchQuery), - dictionaryToArray(pivotState.groupByList), - dictionaryToArray(pivotState.aggList) - ), - // conditionally add optional description - ...(transformDetailsState.transformDescription !== '' - ? { description: transformDetailsState.transformDescription } - : {}), - dest: { - index: transformDetailsState.destinationIndex, - }, - // conditionally add continuous mode config - ...(transformDetailsState.isContinuousModeEnabled - ? { - sync: { - time: { - field: transformDetailsState.continuousModeDateField, - delay: transformDetailsState.continuousModeDelay, - }, +): PutTransformsRequestSchema => ({ + ...getPreviewTransformRequestBody( + indexPatternTitle, + getPivotQuery(pivotState.searchQuery), + dictionaryToArray(pivotState.groupByList), + dictionaryToArray(pivotState.aggList) + ), + // conditionally add optional description + ...(transformDetailsState.transformDescription !== '' + ? { description: transformDetailsState.transformDescription } + : {}), + dest: { + index: transformDetailsState.destinationIndex, + }, + // conditionally add continuous mode config + ...(transformDetailsState.isContinuousModeEnabled + ? { + sync: { + time: { + field: transformDetailsState.continuousModeDateField, + delay: transformDetailsState.continuousModeDelay, }, - } - : {}), - }; - - return request; + }, + } + : {}), +}); + +export function isHttpFetchError(error: any): error is HttpFetchError { + return ( + error instanceof HttpFetchError && + typeof error.name === 'string' && + typeof error.message !== 'undefined' + ); } diff --git a/x-pack/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts index a02bed2fa65e7..b71bab62096b6 100644 --- a/x-pack/plugins/transform/public/app/common/transform.ts +++ b/x-pack/plugins/transform/public/app/common/transform.ts @@ -9,13 +9,7 @@ import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; -import { TransformId } from '../../../common'; - -import { PivotAggDict } from './pivot_aggs'; -import { PivotGroupByDict } from './pivot_group_by'; - -export type IndexName = string; -export type IndexPattern = string; +import { TransformId } from '../../../common/types/transform'; // Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; // It must also start and end with an alphanumeric character. @@ -23,41 +17,6 @@ export function isTransformIdValid(transformId: TransformId) { return /^[a-z0-9\-\_]+$/g.test(transformId) && !/^([_-].*)?(.*[_-])?$/g.test(transformId); } -export interface PreviewRequestBody { - pivot: { - group_by: PivotGroupByDict; - aggregations: PivotAggDict; - }; - source: { - index: IndexPattern | IndexPattern[]; - query?: any; - }; -} - -export interface CreateRequestBody extends PreviewRequestBody { - description?: string; - dest: { - index: IndexName; - }; - frequency?: string; - settings?: { - max_page_search_size?: number; - docs_per_second?: number; - }; - sync?: { - time: { - field: string; - delay: string; - }; - }; -} - -export interface TransformPivotConfig extends CreateRequestBody { - id: TransformId; - create_time?: number; - version?: string; -} - export enum REFRESH_TRANSFORM_LIST_STATE { ERROR = 'error', IDLE = 'idle', diff --git a/x-pack/plugins/transform/public/app/common/transform_list.ts b/x-pack/plugins/transform/public/app/common/transform_list.ts index a2a762a7e2dfb..b32803fea1501 100644 --- a/x-pack/plugins/transform/public/app/common/transform_list.ts +++ b/x-pack/plugins/transform/public/app/common/transform_list.ts @@ -6,9 +6,8 @@ import { EuiTableActionsColumnType } from '@elastic/eui'; -import { TransformId } from '../../../common'; -import { TransformPivotConfig } from './transform'; -import { TransformStats } from './transform_stats'; +import { TransformId, TransformPivotConfig } from '../../../common/types/transform'; +import { TransformStats } from '../../../common/types/transform_stats'; // Used to pass on attribute names to table columns export enum TRANSFORM_LIST_COLUMN { diff --git a/x-pack/plugins/transform/public/app/common/transform_stats.ts b/x-pack/plugins/transform/public/app/common/transform_stats.ts index 72df6d3985e23..aaf7f97399d44 100644 --- a/x-pack/plugins/transform/public/app/common/transform_stats.ts +++ b/x-pack/plugins/transform/public/app/common/transform_stats.ts @@ -4,64 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId, TRANSFORM_STATE } from '../../../common'; +import { TRANSFORM_STATE } from '../../../common/constants'; import { TransformListRow } from './transform_list'; -export enum TRANSFORM_MODE { - BATCH = 'batch', - CONTINUOUS = 'continuous', -} - -export interface TransformStats { - id: TransformId; - checkpointing: { - last: { - checkpoint: number; - timestamp_millis?: number; - }; - next?: { - checkpoint: number; - checkpoint_progress?: { - total_docs: number; - docs_remaining: number; - percent_complete: number; - }; - }; - operations_behind: number; - }; - node?: { - id: string; - name: string; - ephemeral_id: string; - transport_address: string; - attributes: Record; - }; - stats: { - documents_indexed: number; - documents_processed: number; - index_failures: number; - index_time_in_ms: number; - index_total: number; - pages_processed: number; - search_failures: number; - search_time_in_ms: number; - search_total: number; - trigger_count: number; - }; - reason?: string; - state: TRANSFORM_STATE; -} - -export function isTransformStats(arg: any): arg is TransformStats { - return ( - typeof arg === 'object' && - arg !== null && - {}.hasOwnProperty.call(arg, 'state') && - Object.values(TRANSFORM_STATE).includes(arg.state) - ); -} - export function getTransformProgress(item: TransformListRow) { if (isCompletedBatchTransform(item)) { return 100; diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index a5cccd58211c5..40a6ab2b65862 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -4,67 +4,162 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId, TransformEndpointRequest } from '../../../../common'; +import { HttpFetchError } from 'kibana/public'; -import { PreviewRequestBody } from '../../common'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { TransformId } from '../../../../common/types/transform'; +import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms'; +import type { GetTransformsAuditMessagesResponseSchema } from '../../../../common/api_schemas/audit_messages'; +import type { + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../../common/api_schemas/delete_transforms'; +import type { + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../../common/api_schemas/start_transforms'; +import type { + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../../common/api_schemas/stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewRequestSchema, + PostTransformsPreviewResponseSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../../common/api_schemas/transforms'; +import type { GetTransformsStatsResponseSchema } from '../../../../common/api_schemas/transforms_stats'; +import type { + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../../common/api_schemas/update_transforms'; + +import type { SearchResponse7 } from '../../../../common/shared_imports'; +import { EsIndex } from '../../../../common/types/es_index'; + +import type { SavedSearchQuery } from '../use_search_items'; + +// Default sampler shard size used for field histograms +export const DEFAULT_SAMPLER_SHARD_SIZE = 5000; + +export interface FieldHistogramRequestConfig { + fieldName: string; + type?: KBN_FIELD_TYPES; +} const apiFactory = () => ({ - getTransforms(transformId?: TransformId): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransform( + transformId: TransformId + ): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - getTransformsStats(transformId?: TransformId): Promise { - if (transformId !== undefined) { - return new Promise((resolve, reject) => { - resolve([]); - }); - } - - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransforms(): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - createTransform(transformId: TransformId, transformConfig: any): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransformStats( + transformId: TransformId + ): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - deleteTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransformsStats(): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - getTransformsPreview(obj: PreviewRequestBody): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async createTransform( + transformId: TransformId, + transformConfig: PutTransformsRequestSchema + ): Promise { + return Promise.resolve({ transformsCreated: [], errors: [] }); }, - startTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); + async updateTransform( + transformId: TransformId, + transformConfig: PostTransformsUpdateRequestSchema + ): Promise { + return Promise.resolve({ + id: 'the-test-id', + source: { index: ['the-index-name'], query: { match_all: {} } }, + dest: { index: 'user-the-destination-index-name' }, + frequency: '10m', + pivot: { + group_by: { the_group: { terms: { field: 'the-group-by-field' } } }, + aggregations: { the_agg: { value_count: { field: 'the-agg-field' } } }, + }, + description: 'the-description', + settings: { docs_per_second: null }, + version: '8.0.0', + create_time: 1598860879097, }); }, - stopTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async deleteTransforms( + reqBody: DeleteTransformsRequestSchema + ): Promise { + return Promise.resolve({}); }, - getTransformAuditMessages(transformId: TransformId): Promise { - return new Promise((resolve, reject) => { - resolve([]); + async getTransformsPreview( + obj: PostTransformsPreviewRequestSchema + ): Promise { + return Promise.resolve({ + generated_dest_index: { + mappings: { + _meta: { + _transform: { + transform: 'the-transform', + version: { create: 'the-version' }, + creation_date_in_millis: 0, + }, + created_by: 'mock', + }, + properties: {}, + }, + settings: { index: { number_of_shards: '1', auto_expand_replicas: '0-1' } }, + aliases: {}, + }, + preview: [], }); }, - esSearch(payload: any) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async startTransforms( + reqBody: StartTransformsRequestSchema + ): Promise { + return Promise.resolve({}); + }, + async stopTransforms( + transformsInfo: StopTransformsRequestSchema + ): Promise { + return Promise.resolve({}); }, - getIndices() { - return new Promise((resolve, reject) => { - resolve([]); + async getTransformAuditMessages( + transformId: TransformId + ): Promise { + return Promise.resolve([]); + }, + async esSearch(payload: any): Promise { + return Promise.resolve({ + hits: { + hits: [], + total: { + value: 0, + relation: 'the-relation', + }, + max_score: 0, + }, + timed_out: false, + took: 10, + _shards: { total: 1, successful: 1, failed: 0, skipped: 0 }, }); }, + + async getEsIndices(): Promise { + return Promise.resolve([]); + }, + async getHistogramsForFields( + indexPatternTitle: string, + fields: FieldHistogramRequestConfig[], + query: string | SavedSearchQuery, + samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE + ): Promise { + return Promise.resolve([]); + }, }); export const useApi = () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 1d2752b9e939d..4cff5dd9b648e 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -6,20 +6,43 @@ import { useMemo } from 'react'; +import { HttpFetchError } from 'kibana/public'; + import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { - TransformId, - TransformEndpointRequest, - TransformEndpointResult, - DeleteTransformEndpointResult, -} from '../../../common'; +import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages'; +import type { + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../common/api_schemas/delete_transforms'; +import type { FieldHistogramsResponseSchema } from '../../../common/api_schemas/field_histograms'; +import type { + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../common/api_schemas/start_transforms'; +import type { + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../common/api_schemas/stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewRequestSchema, + PostTransformsPreviewResponseSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../common/api_schemas/transforms'; +import type { + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../common/api_schemas/update_transforms'; +import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats'; +import { TransformId } from '../../../common/types/transform'; import { API_BASE_PATH } from '../../../common/constants'; +import { EsIndex } from '../../../common/types/es_index'; +import type { SearchResponse7 } from '../../../common/shared_imports'; import { useAppDependencies } from '../app_dependencies'; -import { GetTransformsResponse, PreviewRequestBody } from '../common'; -import { EsIndex } from './use_api_types'; import { SavedSearchQuery } from './use_search_items'; // Default sampler shard size used for field histograms @@ -35,81 +58,146 @@ export const useApi = () => { return useMemo( () => ({ - getTransforms(transformId?: TransformId): Promise { - const transformIdString = transformId !== undefined ? `/${transformId}` : ''; - return http.get(`${API_BASE_PATH}transforms${transformIdString}`); + async getTransform( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}`); + } catch (e) { + return e; + } }, - getTransformsStats(transformId?: TransformId): Promise { - if (transformId !== undefined) { - return http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`); + async getTransforms(): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms`); + } catch (e) { + return e; } - - return http.get(`${API_BASE_PATH}transforms/_stats`); }, - createTransform(transformId: TransformId, transformConfig: any): Promise { - return http.put(`${API_BASE_PATH}transforms/${transformId}`, { - body: JSON.stringify(transformConfig), - }); + async getTransformStats( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`); + } catch (e) { + return e; + } }, - updateTransform(transformId: TransformId, transformConfig: any): Promise { - return http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, { - body: JSON.stringify(transformConfig), - }); + async getTransformsStats(): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/_stats`); + } catch (e) { + return e; + } }, - deleteTransforms( - transformsInfo: TransformEndpointRequest[], - deleteDestIndex: boolean | undefined, - deleteDestIndexPattern: boolean | undefined, - forceDelete: boolean - ): Promise { - return http.post(`${API_BASE_PATH}delete_transforms`, { - body: JSON.stringify({ - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - }), - }); + async createTransform( + transformId: TransformId, + transformConfig: PutTransformsRequestSchema + ): Promise { + try { + return await http.put(`${API_BASE_PATH}transforms/${transformId}`, { + body: JSON.stringify(transformConfig), + }); + } catch (e) { + return e; + } }, - getTransformsPreview(obj: PreviewRequestBody): Promise { - return http.post(`${API_BASE_PATH}transforms/_preview`, { - body: JSON.stringify(obj), - }); + async updateTransform( + transformId: TransformId, + transformConfig: PostTransformsUpdateRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, { + body: JSON.stringify(transformConfig), + }); + } catch (e) { + return e; + } }, - startTransforms( - transformsInfo: TransformEndpointRequest[] - ): Promise { - return http.post(`${API_BASE_PATH}start_transforms`, { - body: JSON.stringify(transformsInfo), - }); + async deleteTransforms( + reqBody: DeleteTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}delete_transforms`, { + body: JSON.stringify(reqBody), + }); + } catch (e) { + return e; + } }, - stopTransforms(transformsInfo: TransformEndpointRequest[]): Promise { - return http.post(`${API_BASE_PATH}stop_transforms`, { - body: JSON.stringify(transformsInfo), - }); + async getTransformsPreview( + obj: PostTransformsPreviewRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}transforms/_preview`, { + body: JSON.stringify(obj), + }); + } catch (e) { + return e; + } }, - getTransformAuditMessages(transformId: TransformId): Promise { - return http.get(`${API_BASE_PATH}transforms/${transformId}/messages`); + async startTransforms( + reqBody: StartTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}start_transforms`, { + body: JSON.stringify(reqBody), + }); + } catch (e) { + return e; + } + }, + async stopTransforms( + transformsInfo: StopTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}stop_transforms`, { + body: JSON.stringify(transformsInfo), + }); + } catch (e) { + return e; + } + }, + async getTransformAuditMessages( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}/messages`); + } catch (e) { + return e; + } }, - esSearch(payload: any): Promise { - return http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); + async esSearch(payload: any): Promise { + try { + return await http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); + } catch (e) { + return e; + } }, - getIndices(): Promise { - return http.get(`/api/index_management/indices`); + async getEsIndices(): Promise { + try { + return await http.get(`/api/index_management/indices`); + } catch (e) { + return e; + } }, - getHistogramsForFields( + async getHistogramsForFields( indexPatternTitle: string, fields: FieldHistogramRequestConfig[], query: string | SavedSearchQuery, samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE - ) { - return http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, { - body: JSON.stringify({ - query, - fields, - samplerShardSize, - }), - }); + ): Promise { + try { + return await http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, { + body: JSON.stringify({ + query, + fields, + samplerShardSize, + }), + }); + } catch (e) { + return e; + } }, }), [http] diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index fdf77c8ebee51..1a97ba7806fef 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -7,11 +7,11 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import { - DeleteTransformEndpointResult, +import type { DeleteTransformStatus, - TransformEndpointRequest, -} from '../../../common'; + DeleteTransformsRequestSchema, +} from '../../../common/api_schemas/delete_transforms'; +import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; import { extractErrorMessage } from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; @@ -109,173 +109,157 @@ export const useDeleteTransforms = () => { const toastNotifications = useToastNotifications(); const api = useApi(); - return async ( - transforms: TransformListRow[], - shouldDeleteDestIndex: boolean, - shouldDeleteDestIndexPattern: boolean, - shouldForceDelete = false - ) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({ - id: tf.config.id, - state: tf.stats.state, - })); + return async (reqBody: DeleteTransformsRequestSchema) => { + const results = await api.deleteTransforms(reqBody); - try { - const results: DeleteTransformEndpointResult = await api.deleteTransforms( - transformsInfo, - shouldDeleteDestIndex, - shouldDeleteDestIndexPattern, - shouldForceDelete - ); - const isBulk = Object.keys(results).length > 1; - const successCount: Record = { - transformDeleted: 0, - destIndexDeleted: 0, - destIndexPatternDeleted: 0, - }; - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const status = results[transformId]; - const destinationIndex = status.destinationIndex; + if (!isDeleteTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { + defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', + }), + text: toMountPoint( + + ), + }); + return; + } - // if we are only deleting one transform, show the success toast messages - if (!isBulk && status.transformDeleted) { - if (status.transformDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', { - defaultMessage: 'Request to delete transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - } - if (status.destIndexDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage', - { - defaultMessage: - 'Request to delete destination index {destinationIndex} acknowledged.', - values: { destinationIndex }, - } - ) - ); - } - if (status.destIndexPatternDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage', - { - defaultMessage: - 'Request to delete index pattern {destinationIndex} acknowledged.', - values: { destinationIndex }, - } - ) - ); - } - } else { - (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { - if (status[key]?.success) { - successCount[key] = successCount[key] + 1; - } - }); - } - if (status.transformDeleted?.error) { - const error = extractErrorMessage(status.transformDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { - defaultMessage: 'An error occurred deleting the transform {transformId}', + const isBulk = Object.keys(results).length > 1; + const successCount: Record = { + transformDeleted: 0, + destIndexDeleted: 0, + destIndexPatternDeleted: 0, + }; + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const status = results[transformId]; + const destinationIndex = status.destinationIndex; + + // if we are only deleting one transform, show the success toast messages + if (!isBulk && status.transformDeleted) { + if (status.transformDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', { + defaultMessage: 'Request to delete transform {transformId} acknowledged.', values: { transformId }, - }), - text: toMountPoint( - - ), - }); + }) + ); } - - if (status.destIndexDeleted?.error) { - const error = extractErrorMessage(status.destIndexDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', + if (status.destIndexDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage', { - defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + defaultMessage: + 'Request to delete destination index {destinationIndex} acknowledged.', values: { destinationIndex }, } - ), - text: toMountPoint( - - ), - }); + ) + ); } - - if (status.destIndexPatternDeleted?.error) { - const error = extractErrorMessage(status.destIndexPatternDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage', + if (status.destIndexPatternDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage', { - defaultMessage: 'An error occurred deleting index pattern {destinationIndex}', + defaultMessage: + 'Request to delete index pattern {destinationIndex} acknowledged.', values: { destinationIndex }, } - ), - text: toMountPoint( - - ), - }); + ) + ); } + } else { + (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { + if (status[key]?.success) { + successCount[key] = successCount[key] + 1; + } + }); } - } - - // if we are deleting multiple transforms, combine the success messages - if (isBulk) { - if (successCount.transformDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.', - values: { count: successCount.transformDeleted }, - }) - ); + if (status.transformDeleted?.error) { + const error = extractErrorMessage(status.transformDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { + defaultMessage: 'An error occurred deleting the transform {transformId}', + values: { transformId }, + }), + text: toMountPoint( + + ), + }); } - if (successCount.destIndexDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.', - values: { count: successCount.destIndexDeleted }, - }) - ); + if (status.destIndexDeleted?.error) { + const error = extractErrorMessage(status.destIndexDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', + { + defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + values: { destinationIndex }, + } + ), + text: toMountPoint( + + ), + }); } - if (successCount.destIndexPatternDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage', + + if (status.destIndexPatternDeleted?.error) { + const error = extractErrorMessage(status.destIndexPatternDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage', { - defaultMessage: - 'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.', - values: { count: successCount.destIndexPatternDeleted }, + defaultMessage: 'An error occurred deleting index pattern {destinationIndex}', + values: { destinationIndex }, } - ) - ); + ), + text: toMountPoint( + + ), + }); } } + } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { - defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', - }), - text: toMountPoint( - - ), - }); + // if we are deleting multiple transforms, combine the success messages + if (isBulk) { + if (successCount.transformDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.', + values: { count: successCount.transformDeleted }, + }) + ); + } + + if (successCount.destIndexDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.', + values: { count: successCount.destIndexDeleted }, + }) + ); + } + if (successCount.destIndexPatternDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.', + values: { count: successCount.destIndexPatternDeleted }, + }) + ); + } } + + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts index bd19a7f8bf4d8..5f3a9a6abfdb4 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts @@ -4,52 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - TransformListRow, - TransformStats, - TRANSFORM_MODE, - isTransformStats, - TransformPivotConfig, - refreshTransformList$, - REFRESH_TRANSFORM_LIST_STATE, -} from '../common'; - -import { useApi } from './use_api'; +import { HttpFetchError } from 'src/core/public'; -interface GetTransformsResponse { - count: number; - transforms: TransformPivotConfig[]; -} - -interface GetTransformsStatsResponseOk { - node_failures?: object; - count: number; - transforms: TransformStats[]; -} - -const isGetTransformsStatsResponseOk = (arg: any): arg is GetTransformsStatsResponseOk => { - return ( - {}.hasOwnProperty.call(arg, 'count') && - {}.hasOwnProperty.call(arg, 'transforms') && - Array.isArray(arg.transforms) - ); -}; +import { + isGetTransformsResponseSchema, + isGetTransformsStatsResponseSchema, +} from '../../../common/api_schemas/type_guards'; +import { TRANSFORM_MODE } from '../../../common/constants'; +import { isTransformStats } from '../../../common/types/transform_stats'; -interface GetTransformsStatsResponseError { - statusCode: number; - error: string; - message: string; -} +import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; -type GetTransformsStatsResponse = GetTransformsStatsResponseOk | GetTransformsStatsResponseError; +import { useApi } from './use_api'; export type GetTransforms = (forceRefresh?: boolean) => void; export const useGetTransforms = ( setTransforms: React.Dispatch>, - setErrorMessage: React.Dispatch< - React.SetStateAction - >, + setErrorMessage: React.Dispatch>, setIsInitialized: React.Dispatch>, blockRefresh: boolean ): GetTransforms => { @@ -66,45 +38,57 @@ export const useGetTransforms = ( return; } - try { - const transformConfigs: GetTransformsResponse = await api.getTransforms(); - const transformStats: GetTransformsStatsResponse = await api.getTransformsStats(); - - const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => { - const stats = isGetTransformsStatsResponseOk(transformStats) - ? transformStats.transforms.find((d) => config.id === d.id) - : undefined; - - // A newly created transform might not have corresponding stats yet. - // If that's the case we just skip the transform and don't add it to the transform list yet. - if (!isTransformStats(stats)) { - return reducedtableRows; - } - - // Table with expandable rows requires `id` on the outer most level - reducedtableRows.push({ - id: config.id, - config, - mode: - typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH, - stats, - }); - return reducedtableRows; - }, [] as TransformListRow[]); + const transformConfigs = await api.getTransforms(); + const transformStats = await api.getTransformsStats(); - setTransforms(tableRows); - setErrorMessage(undefined); - setIsInitialized(true); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); - } catch (e) { + if ( + !isGetTransformsResponseSchema(transformConfigs) || + !isGetTransformsStatsResponseSchema(transformStats) + ) { // An error is followed immediately by setting the state to idle. // This way we're able to treat ERROR as a one-time-event like REFRESH. refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR); refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); setTransforms([]); - setErrorMessage(e); + setIsInitialized(true); + + if (!isGetTransformsResponseSchema(transformConfigs)) { + setErrorMessage(transformConfigs); + } else if (!isGetTransformsStatsResponseSchema(transformStats)) { + setErrorMessage(transformStats); + } + + return; } + + const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => { + const stats = isGetTransformsStatsResponseSchema(transformStats) + ? transformStats.transforms.find((d) => config.id === d.id) + : undefined; + + // A newly created transform might not have corresponding stats yet. + // If that's the case we just skip the transform and don't add it to the transform list yet. + if (!isTransformStats(stats)) { + return reducedtableRows; + } + + // Table with expandable rows requires `id` on the outer most level + reducedtableRows.push({ + id: config.id, + config, + mode: + typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH, + stats, + }); + return reducedtableRows; + }, [] as TransformListRow[]); + + setTransforms(tableRows); + setErrorMessage(undefined); + setIsInitialized(true); + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); + concurrentLoads--; if (concurrentLoads > 0) { diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 946f7991d049d..ce233d0cf7caa 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -8,6 +8,11 @@ import { useEffect } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; +import { + isEsSearchResponse, + isFieldHistogramsResponseSchema, +} from '../../../common/api_schemas/type_guards'; + import { getFieldType, getDataGridSchemaFromKibanaFieldType, @@ -16,7 +21,6 @@ import { useDataGrid, useRenderCellValue, EsSorting, - SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, } from '../../shared_imports'; @@ -29,8 +33,6 @@ import { useApi } from './use_api'; import { useToastNotifications } from '../app_dependencies'; -type IndexSearchResponse = SearchResponse7; - export const useIndexData = ( indexPattern: SearchItems['indexPattern'], query: PivotQuery @@ -90,37 +92,39 @@ export const useIndexData = ( }, }; - try { - const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map((d) => d._source); + const resp = await api.esSearch(esSearchRequest); - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - setErrorMessage(getErrorMessage(e)); + if (!isEsSearchResponse(resp)) { + setErrorMessage(getErrorMessage(resp)); setStatus(INDEX_STATUS.ERROR); return; } + + const docs = resp.hits.hits.map((d) => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); }; const fetchColumnChartsData = async function () { - try { - const columnChartsData = await api.getHistogramsForFields( - indexPattern.title, - columns - .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) - .map((cT) => ({ - fieldName: cT.id, - type: getFieldType(cT.schema), - })), - isDefaultQuery(query) ? matchAllQuery : query - ); - setColumnCharts(columnChartsData); - } catch (e) { - showDataGridColumnChartErrorMessageToast(e, toastNotifications); + const columnChartsData = await api.getHistogramsForFields( + indexPattern.title, + columns + .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + .map((cT) => ({ + fieldName: cT.id, + type: getFieldType(cT.schema), + })), + isDefaultQuery(query) ? matchAllQuery : query + ); + + if (!isFieldHistogramsResponseSchema(columnChartsData)) { + showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications); + return; } + + setColumnCharts(columnChartsData); }; useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index a0e7c5dde494a..c51bf7d7e6741 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -13,11 +13,13 @@ import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; +import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms'; +import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; import { dictionaryToArray } from '../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../shared_imports'; import { getNestedProperty } from '../../../common/utils/object_utils'; import { + formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, RenderCellValue, @@ -27,12 +29,11 @@ import { import { getErrorMessage } from '../../../common/utils/errors'; import { - getPreviewRequestBody, + getPreviewTransformRequestBody, PivotAggsConfigDict, PivotGroupByConfigDict, PivotGroupByConfig, PivotQuery, - PreviewMappings, PivotAggsConfig, } from '../common'; @@ -74,21 +75,23 @@ export const usePivotData = ( aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict ): UseIndexDataReturnType => { - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const [previewMappingsProperties, setPreviewMappingsProperties] = useState< + PreviewMappingsProperties + >({}); const api = useApi(); const aggsArr = useMemo(() => dictionaryToArray(aggs), [aggs]); const groupByArr = useMemo(() => dictionaryToArray(groupBy), [groupBy]); // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - (key) => previewMappings.properties[key].type !== 'object' + const columnKeys = Object.keys(previewMappingsProperties).filter( + (key) => previewMappingsProperties[key].type !== 'object' ); columnKeys.sort(sortColumns(groupByArr)); // EuiDataGrid State const columns: EuiDataGridColumn[] = columnKeys.map((id) => { - const field = previewMappings.properties[id]; + const field = previewMappingsProperties[id]; // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. @@ -159,28 +162,35 @@ export const usePivotData = ( setNoDataMessage(''); setStatus(INDEX_STATUS.LOADING); - try { - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - const resp = await api.getTransformsPreview(previewRequest); - setTableItems(resp.preview); - setRowCount(resp.preview.length); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(INDEX_STATUS.LOADED); - - if (resp.preview.length === 0) { - setNoDataMessage( - i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - }) - ); - } - } catch (e) { - setErrorMessage(getErrorMessage(e)); + const previewRequest = getPreviewTransformRequestBody( + indexPatternTitle, + query, + groupByArr, + aggsArr + ); + const resp = await api.getTransformsPreview(previewRequest); + + if (!isPostTransformsPreviewResponseSchema(resp)) { + setErrorMessage(getErrorMessage(resp)); setTableItems([]); setRowCount(0); - setPreviewMappings({ properties: {} }); + setPreviewMappingsProperties({}); setStatus(INDEX_STATUS.ERROR); + return; + } + + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappingsProperties(resp.generated_dest_index.mappings.properties); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); } }; @@ -236,19 +246,19 @@ export const usePivotData = ( if ( [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type + previewMappingsProperties[columnId].type ) ) { return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); } - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + if (previewMappingsProperties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { return cellValue ? 'true' : 'false'; } return cellValue; }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]); return { ...dataGrid, diff --git a/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx similarity index 52% rename from x-pack/plugins/transform/public/app/hooks/use_start_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx index a0ffe1fdfa336..71ed220b6b4df 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx @@ -4,25 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms'; +import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; + +import { getErrorMessage } from '../../../common/utils/errors'; -import { useToastNotifications } from '../app_dependencies'; -import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStartTransforms = () => { + const deps = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); - return async (transforms: TransformListRow[]) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({ - id: tf.config.id, - state: tf.stats.state, - })); - const results: TransformEndpointResult = await api.startTransforms(transformsInfo); + return async (transformsInfo: StartTransformsRequestSchema) => { + const results = await api.startTransforms(transformsInfo); + + if (!isStartTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage', + { + defaultMessage: 'An error occurred calling the start transforms request.', + } + ), + text: toMountPoint( + + ), + }); + return; + } for (const transformId in results) { // hasOwnProperty check to ensure only properties on object itself, and not its prototypes diff --git a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx similarity index 53% rename from x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx index 0df9834647704..be223c5eddfdd 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx @@ -4,25 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms'; +import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; + +import { getErrorMessage } from '../../../common/utils/errors'; -import { useToastNotifications } from '../app_dependencies'; -import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStopTransforms = () => { + const deps = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); - return async (transforms: TransformListRow[]) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((df) => ({ - id: df.config.id, - state: df.stats.state, - })); - const results: TransformEndpointResult = await api.stopTransforms(transformsInfo); + return async (transformsInfo: StopTransformsRequestSchema) => { + const results = await api.stopTransforms(transformsInfo); + + if (!isStopTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage', + { + defaultMessage: 'An error occurred called the stop transforms request.', + } + ), + text: toMountPoint( + + ), + }); + return; + } for (const transformId in results) { // hasOwnProperty check to ensure only properties on object itself, and not its prototypes diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 6553d4474d392..790fcaf5fa83c 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -6,7 +6,7 @@ import React, { createContext } from 'react'; -import { Privileges } from '../../../../../common'; +import { Privileges } from '../../../../../common/types/privileges'; import { useRequest } from '../../../hooks'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index 282a737d0bf1e..841c6ed01766a 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { Privileges } from '../../../../../common'; +import { Privileges } from '../../../../../common/types/privileges'; export interface Capabilities { canGetTransform: boolean; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx index 89c6ac3a054f7..beeacc76bdc95 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx @@ -10,7 +10,7 @@ import { EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MissingPrivileges } from '../../../../../common'; +import { MissingPrivileges } from '../../../../../common/types/privileges'; import { SectionLoading } from '../../../components'; diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 19ba31d36e6e9..9a97c66bfb10b 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -21,40 +21,20 @@ import { EuiTitle, } from '@elastic/eui'; +import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { TransformPivotConfig } from '../../../../common/types/transform'; + +import { isHttpFetchError } from '../../common/request'; import { useApi } from '../../hooks/use_api'; import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; -import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; - import { useAppDependencies } from '../../app_dependencies'; -import { TransformPivotConfig } from '../../common'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { Wizard } from '../create_transform/components/wizard'; -interface GetTransformsResponseOk { - count: number; - transforms: TransformPivotConfig[]; -} - -interface GetTransformsResponseError { - error: { - msg: string; - path: string; - query: any; - statusCode: number; - response: string; - }; -} - -function isGetTransformsResponseError(arg: any): arg is GetTransformsResponseError { - return arg.error !== undefined; -} - -type GetTransformsResponse = GetTransformsResponseOk | GetTransformsResponseError; - type Props = RouteComponentProps<{ transformId: string }>; export const CloneTransformSection: FC = ({ match }) => { // Set breadcrumb and page title @@ -84,15 +64,15 @@ export const CloneTransformSection: FC = ({ match }) => { } = useSearchItems(undefined); const fetchTransformConfig = async () => { - try { - const transformConfigs: GetTransformsResponse = await api.getTransforms(transformId); - if (isGetTransformsResponseError(transformConfigs)) { - setTransformConfig(undefined); - setErrorMessage(transformConfigs.error.msg); - setIsInitialized(true); - return; - } + const transformConfigs = await api.getTransform(transformId); + if (isHttpFetchError(transformConfigs)) { + setTransformConfig(undefined); + setErrorMessage(transformConfigs.message); + setIsInitialized(true); + return; + } + try { await loadIndexPatterns(savedObjectsClient, indexPatterns); const indexPatternTitle = Array.isArray(transformConfigs.transforms[0].source.index) ? transformConfigs.transforms[0].source.index.join(',') diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx index fa0fe7bdf6126..49d59706befb8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx @@ -7,7 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggLabelForm } from './agg_label_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx index e50ba9e137331..4e5e3f71cd6e2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, isPivotAggsConfigWithUiSupport, PivotAggsConfig, PivotAggsConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx index 32c7ca5972e00..93de3d4fcfc9f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggListForm, AggListProps } from './list_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx index a02f4455250d7..f6ae1f292b0e6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, PivotAggsConfig, PivotAggsConfigDict, PivotAggsConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx index 923d52ba5cec1..8c644c358e658 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggListSummary, AggListSummaryProps } from './list_summary'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx index 7d07d79e7d283..fb6e141a54b04 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx @@ -8,7 +8,9 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { AggName, PivotAggsConfigDict } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; + +import { PivotAggsConfigDict } from '../../../../common'; export interface AggListSummaryProps { list: PivotAggsConfigDict; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx index b3e770a269681..8f2fbfb7084e6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx @@ -7,7 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PIVOT_SUPPORTED_AGGS, PivotAggsConfig } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { PopoverForm } from './popover_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 50064274cf98e..30e8c2b594db7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -20,10 +20,14 @@ import { import { cloneDeep } from 'lodash'; import { useUpdateEffect } from 'react-use'; +import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; +import { + PivotSupportedAggs, + PIVOT_SUPPORTED_AGGS, +} from '../../../../../../common/types/pivot_aggs'; import { - AggName, isAggName, isPivotAggsConfigPercentiles, isPivotAggsConfigWithUiSupport, @@ -31,9 +35,8 @@ import { PERCENTILES_AGG_DEFAULT_PERCENTS, PivotAggsConfig, PivotAggsConfigWithUiSupportDict, - PIVOT_SUPPORTED_AGGS, } from '../../../../common'; -import { isPivotAggsWithExtendedForm, PivotSupportedAggs } from '../../../../common/pivot_aggs'; +import { isPivotAggsWithExtendedForm } from '../../../../common/pivot_aggs'; import { getAggFormConfig } from '../step_define/common/get_agg_form_config'; interface Props { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx index c79da06ac8080..ff66ed6779e14 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, isGroupByDateHistogram, isGroupByHistogram, PivotGroupByConfig, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx index 2dc1a4332f6ad..a60989c76ab13 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, PivotGroupByConfig, PivotGroupByConfigDict, PivotGroupByConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx index 090f3b19f47fb..13829222f11f5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; + +import { PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common'; import { isIntervalValid, PopoverForm } from './popover_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx index 0452638e90dfb..f0a96fa6ab875 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx @@ -18,10 +18,10 @@ import { EuiSpacer, } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; import { - AggName, dateHistogramIntervalFormatRegex, getEsAggFromGroupByConfig, isGroupByDateHistogram, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 2fa1b7c713370..675bd0f9f88ed 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -30,6 +30,12 @@ import { import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; +import { + isGetTransformsStatsResponseSchema, + isPutTransformsResponseSchema, + isStartTransformsResponseSchema, +} from '../../../../../../common/api_schemas/type_guards'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { getErrorMessage } from '../../../../../../common/utils/errors'; @@ -93,34 +99,28 @@ export const StepCreateForm: FC = React.memo( async function createTransform() { setLoading(true); - try { - const resp = await api.createTransform(transformId, transformConfig); - if (resp.errors !== undefined && Array.isArray(resp.errors)) { - if (resp.errors.length === 1) { - throw resp.errors[0]; - } + const resp = await api.createTransform(transformId, transformConfig); - if (resp.errors.length > 1) { - throw resp.errors; - } + if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) { + let respErrors: + | PutTransformsResponseSchema['errors'] + | PutTransformsResponseSchema['errors'][number] + | undefined; + + if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) { + respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors; } - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', { - defaultMessage: 'Request to create transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - setCreated(true); - setLoading(false); - } catch (e) { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', { defaultMessage: 'An error occurred creating the transform {transformId}:', values: { transformId }, }), text: toMountPoint( - + ), }); setCreated(false); @@ -128,6 +128,15 @@ export const StepCreateForm: FC = React.memo( return false; } + toastNotifications.addSuccess( + i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', { + defaultMessage: 'Request to create transform {transformId} acknowledged.', + values: { transformId }, + }) + ); + setCreated(true); + setLoading(false); + if (createIndexPattern) { createKibanaIndexPattern(); } @@ -138,37 +147,36 @@ export const StepCreateForm: FC = React.memo( async function startTransform() { setLoading(true); - try { - const resp = await api.startTransforms([{ id: transformId }]); - if (typeof resp === 'object' && resp !== null && resp[transformId]?.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { - defaultMessage: 'Request to start transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - setStarted(true); - setLoading(false); - } else { - const errorMessage = - typeof resp === 'object' && resp !== null && resp[transformId]?.success === false - ? resp[transformId].error - : resp; - throw new Error(errorMessage); - } - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { - defaultMessage: 'An error occurred starting the transform {transformId}:', + const resp = await api.startTransforms([{ id: transformId }]); + + if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { + defaultMessage: 'Request to start transform {transformId} acknowledged.', values: { transformId }, - }), - text: toMountPoint( - - ), - }); - setStarted(false); + }) + ); + setStarted(true); setLoading(false); + return; } + + const errorMessage = + isStartTransformsResponseSchema(resp) && resp[transformId]?.success === false + ? resp[transformId].error + : resp; + + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { + defaultMessage: 'An error occurred starting the transform {transformId}:', + values: { transformId }, + }), + text: toMountPoint( + + ), + }); + setStarted(false); + setLoading(false); } async function createAndStartTransform() { @@ -250,27 +258,30 @@ export const StepCreateForm: FC = React.memo( // wrapping in function so we can keep the interval id in local scope function startProgressBar() { const interval = setInterval(async () => { - try { - const stats = await api.getTransformsStats(transformId); - if (stats && Array.isArray(stats.transforms) && stats.transforms.length > 0) { - const percent = - getTransformProgress({ - id: transformConfig.id, - config: transformConfig, - stats: stats.transforms[0], - }) || 0; - setProgressPercentComplete(percent); - if (percent >= 100) { - clearInterval(interval); - } + const stats = await api.getTransformStats(transformId); + + if ( + isGetTransformsStatsResponseSchema(stats) && + Array.isArray(stats.transforms) && + stats.transforms.length > 0 + ) { + const percent = + getTransformProgress({ + id: transformConfig.id, + config: transformConfig, + stats: stats.transforms[0], + }) || 0; + setProgressPercentComplete(percent); + if (percent >= 100) { + clearInterval(interval); } - } catch (e) { + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', { defaultMessage: 'An error occurred getting the progress percentage:', }), text: toMountPoint( - + ), }); clearInterval(interval); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts index fba703b1540f9..1523a0d9a89f9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -6,19 +6,21 @@ import { isEqual } from 'lodash'; +import { Dictionary } from '../../../../../../../common/types/common'; +import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs'; +import { TransformPivotConfig } from '../../../../../../../common/types/transform'; + import { matchAllQuery, PivotAggsConfig, PivotAggsConfigDict, PivotGroupByConfig, PivotGroupByConfigDict, - TransformPivotConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; import { StepDefineExposedState } from './types'; -import { getAggConfigFromEsAgg, PivotSupportedAggs } from '../../../../../common/pivot_aggs'; +import { getAggConfigFromEsAgg } from '../../../../../common/pivot_aggs'; export function applyTransformConfigToDefineState( state: StepDefineExposedState, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index 9d3ab44aa5708..d59f99192621c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; import { useUpdateEffect } from 'react-use'; import { i18n } from '@kbn/i18n'; +import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; import { useApi } from '../../../../../../../hooks'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { FilterAggConfigTerm } from '../types'; @@ -55,22 +56,24 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm }, }; - try { - const response = await api.esSearch(esSearchRequest); - setOptions( - response.aggregations.field_values.buckets.map( - (value: { key: string; doc_count: number }) => ({ label: value.key }) - ) - ); - } catch (e) { + const response = await api.esSearch(esSearchRequest); + + setIsLoading(false); + + if (!isEsSearchResponse(response)) { toastNotifications.addWarning( i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', { defaultMessage: 'Unable to fetch suggestions', }) ); + return; } - setIsLoading(false); + setOptions( + response.aggregations.field_values.buckets.map( + (value: { key: string; doc_count: number }) => ({ label: value.key }) + ) + ); }, 600), [selectedField] ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts index 2839c1181c333..5575e6d814daf 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts @@ -5,11 +5,11 @@ */ import { - PIVOT_SUPPORTED_AGGS, - PivotAggsConfigBase, - PivotAggsConfigWithUiBase, PivotSupportedAggs, -} from '../../../../../common/pivot_aggs'; + PIVOT_SUPPORTED_AGGS, +} from '../../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfigBase, PivotAggsConfigWithUiBase } from '../../../../../common/pivot_aggs'; import { getFilterAggConfig } from './filter_agg/config'; /** diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts index 57f9397089f1d..03cbf2e358736 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts @@ -6,7 +6,8 @@ import { i18n } from '@kbn/i18n'; -import { AggName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { AggName } from '../../../../../../../common/types/aggregations'; +import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; export function getAggNameConflictToastMessages( aggName: AggName, @@ -36,7 +37,7 @@ export function getAggNameConflictToastMessages( // check the new aggName against existing aggs and groupbys const aggNameSplit = aggName.split('.'); let aggNameCheck: string; - aggNameSplit.forEach((aggNamePart) => { + aggNameSplit.forEach((aggNamePart: string) => { aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { conflicts.push( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts index 460164c9afe73..14c03aebe892a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EsFieldName } from '../../../../../../../common/types/fields'; import { - EsFieldName, - PERCENTILES_AGG_DEFAULT_PERCENTS, + PivotSupportedAggs, PIVOT_SUPPORTED_AGGS, +} from '../../../../../../../common/types/pivot_aggs'; +import { + PERCENTILES_AGG_DEFAULT_PERCENTS, PivotAggsConfigWithUiSupport, } from '../../../../../common'; -import { PivotSupportedAggs } from '../../../../../common/pivot_aggs'; import { getFilterAggConfig } from './filter_agg/config'; /** diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts index 712a745ff6e77..657e8c935b875 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EsFieldName, - GroupByConfigWithUiSupport, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../../../../common'; +import { EsFieldName } from '../../../../../../../common/types/fields'; + +import { GroupByConfigWithUiSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../../common'; export function getDefaultGroupByConfig( aggName: string, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 56fde98cd4c71..955982aae6007 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -6,7 +6,9 @@ import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { EsFieldName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { EsFieldName } from '../../../../../../../common/types/fields'; + +import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; import { SavedSearchQuery } from '../../../../../hooks/use_search_items'; import { QUERY_LANGUAGE } from './constants'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts index 2e92114286599..41b84f04db852 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts @@ -8,13 +8,13 @@ import { useEffect, useState } from 'react'; import { useXJsonMode } from '../../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PreviewRequestBody } from '../../../../../common'; +import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms'; import { StepDefineExposedState } from '../common'; export const useAdvancedPivotEditor = ( defaults: StepDefineExposedState, - previewRequest: PreviewRequestBody + previewRequest: PostTransformsPreviewRequestSchema ) => { const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts index 1ea8a45248fb9..3f930711b970a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts @@ -6,13 +6,13 @@ import { useState } from 'react'; -import { PreviewRequestBody } from '../../../../../common'; +import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms'; import { StepDefineExposedState } from '../common'; export const useAdvancedSourceEditor = ( defaults: StepDefineExposedState, - previewRequest: PreviewRequestBody + previewRequest: PostTransformsPreviewRequestSchema ) => { const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts index d35d567fc8469..90b28f0e305a5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -6,11 +6,11 @@ import { useCallback, useMemo, useState } from 'react'; +import { AggName } from '../../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../../common/types/common'; import { useToastNotifications } from '../../../../../app_dependencies'; import { - AggName, DropDownLabel, PivotAggsConfig, PivotAggsConfigDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts index f5980ae2243d3..7c10201fc3a6e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -6,7 +6,7 @@ import { useEffect } from 'react'; -import { getPreviewRequestBody } from '../../../../../common'; +import { getPreviewTransformRequestBody } from '../../../../../common'; import { getDefaultStepDefineState } from '../common'; @@ -26,7 +26,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const searchBar = useSearchBar(defaults, indexPattern); const pivotConfig = usePivotConfig(defaults, indexPattern); - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, pivotConfig.state.pivotGroupByArr, @@ -41,7 +41,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi useEffect(() => { if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { - const previewRequestUpdate = getPreviewRequestBody( + const previewRequestUpdate = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, pivotConfig.state.pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 8c919a5185d7e..986ac0a212e8a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -15,10 +15,11 @@ import { coreMock } from '../../../../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../../../../src/plugins/data/public/mocks'; const startMock = coreMock.createStart(); +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + import { PivotAggsConfigDict, PivotGroupByConfigDict, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index e0b350542a8f8..10f473074b4d7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -22,6 +22,9 @@ import { EuiText, } from '@elastic/eui'; +import { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; +import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; + import { DataGrid } from '../../../../../shared_imports'; import { @@ -30,10 +33,8 @@ import { } from '../../../../common/data_grid'; import { - getPreviewRequestBody, - PivotAggDict, + getPreviewTransformRequestBody, PivotAggsConfigDict, - PivotGroupByDict, PivotGroupByConfigDict, PivotSupportedGroupByAggs, PivotAggsConfig, @@ -87,7 +88,7 @@ export const StepDefineForm: FC = React.memo((props) => { toastNotifications, }; - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( indexPattern.title, pivotQuery, pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index dc3d950938c9e..f8a060e0007b8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -7,10 +7,11 @@ import React from 'react'; import { render, wait } from '@testing-library/react'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + import { PivotAggsConfig, PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 414f6e37504da..fa4f8a7e09690 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -18,7 +18,7 @@ import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, getPivotPreviewDevConsoleStatement, - getPreviewRequestBody, + getPreviewTransformRequestBody, isDefaultQuery, isMatchAllQuery, } from '../../../../common'; @@ -44,7 +44,7 @@ export const StepDefineSummary: FC = ({ const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( searchItems.indexPattern.title, pivotQuery, pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 85f4065e8c069..43d4f11cffc9d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -11,24 +11,28 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { TransformId } from '../../../../../../common'; + +import { + isEsIndices, + isPostTransformsPreviewResponseSchema, +} from '../../../../../../common/api_schemas/type_guards'; +import { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; import { getErrorMessage } from '../../../../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; +import { isHttpFetchError } from '../../../../common/request'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SearchItems } from '../../../../hooks/use_search_items'; import { useApi } from '../../../../hooks/use_api'; import { StepDetailsTimeField } from './step_details_time_field'; import { getPivotQuery, - getPreviewRequestBody, + getPreviewTransformRequestBody, isTransformIdValid, - TransformPivotConfig, } from '../../../../common'; import { EsIndexName, IndexPatternTitle } from './common'; import { delayValidator } from '../../../../common/validators'; @@ -48,10 +52,12 @@ export interface StepDetailsExposedState { indexPatternDateField?: string | undefined; } +const defaultContinuousModeDelay = '60s'; + export function getDefaultStepDetailsState(): StepDetailsExposedState { return { continuousModeDateField: '', - continuousModeDelay: '60s', + continuousModeDelay: defaultContinuousModeDelay, createIndexPattern: true, isContinuousModeEnabled: false, transformId: '', @@ -72,7 +78,7 @@ export function applyTransformConfigToDetailsState( const time = transformConfig.sync?.time; if (time !== undefined) { state.continuousModeDateField = time.field; - state.continuousModeDelay = time.delay; + state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay; state.isContinuousModeEnabled = true; } } @@ -137,19 +143,20 @@ export const StepDetailsForm: FC = React.memo( useEffect(() => { // use an IIFE to avoid returning a Promise to useEffect. (async function () { - try { - const { searchQuery, groupByList, aggList } = stepDefineState; - const pivotAggsArr = dictionaryToArray(aggList); - const pivotGroupByArr = dictionaryToArray(groupByList); - const pivotQuery = getPivotQuery(searchQuery); - const previewRequest = getPreviewRequestBody( - searchItems.indexPattern.title, - pivotQuery, - pivotGroupByArr, - pivotAggsArr - ); - - const transformPreview = await api.getTransformsPreview(previewRequest); + const { searchQuery, groupByList, aggList } = stepDefineState; + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); + const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewTransformRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const transformPreview = await api.getTransformsPreview(previewRequest); + + if (isPostTransformsPreviewResponseSchema(transformPreview)) { const properties = transformPreview.generated_dest_index.mappings.properties; const datetimeColumns: string[] = Object.keys(properties).filter( (col) => properties[col].type === 'date' @@ -157,43 +164,46 @@ export const StepDetailsForm: FC = React.memo( setPreviewDateColumns(datetimeColumns); setIndexPatternDateField(datetimeColumns[0]); - } catch (e) { + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', { - defaultMessage: 'An error occurred getting transform preview', + defaultMessage: 'An error occurred fetching the transform preview', }), text: toMountPoint( - + ), }); } - try { - setTransformIds( - (await api.getTransforms()).transforms.map( - (transform: TransformPivotConfig) => transform.id - ) - ); - } catch (e) { + const resp = await api.getTransforms(); + + if (isHttpFetchError(resp)) { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { defaultMessage: 'An error occurred getting the existing transform IDs:', }), text: toMountPoint( - + ), }); + } else { + setTransformIds(resp.transforms.map((transform: TransformPivotConfig) => transform.id)); } - try { - setIndexNames((await api.getIndices()).map((index) => index.name)); - } catch (e) { + const indices = await api.getEsIndices(); + + if (isEsIndices(indices)) { + setIndexNames(indices.map((index) => index.name)); + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { defaultMessage: 'An error occurred getting the existing index names:', }), text: toMountPoint( - + ), }); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 806dcbfa75604..0ca018972cac9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; -import { getCreateRequestBody, TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + +import { getCreateTransformRequestBody } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; import { @@ -149,7 +151,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) } }, []); - const transformConfig = getCreateRequestBody( + const transformConfig = getCreateTransformRequestBody( indexPattern.title, stepDefineState, stepDetailsState diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx index d8ab72f15c59c..75868fb8fcabd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants'; import { createCapabilityFailureMessage } from '../../../../lib/authorization'; import { TransformListRow } from '../../../../common'; @@ -18,8 +18,8 @@ export const deleteActionNameText = i18n.translate( } ); -const transformCanNotBeDeleted = (item: TransformListRow) => - ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(item.stats.state); +const transformCanNotBeDeleted = (i: TransformListRow) => + !([TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED] as TransformState[]).includes(i.stats.state); export const isDeleteActionDisabled = (items: TransformListRow[], forceDisable: boolean) => { const disabled = items.some(transformCanNotBeDeleted); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx index e573709fa6e63..7e8e099b69f82 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx @@ -6,7 +6,7 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListAction, TransformListRow } from '../../../../common'; import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; @@ -55,7 +55,16 @@ export const useDeleteAction = (forceDisable: boolean) => { const forceDelete = isBulkAction ? shouldForceDelete : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; - deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); + + deleteTransforms({ + transformsInfo: items.map((i) => ({ + id: i.config.id, + state: i.stats.state, + })), + deleteDestIndex: shouldDeleteDestIndex, + deleteDestIndexPattern: shouldDeleteDestIndexPattern, + forceDelete, + }); }; const openModal = (newItems: TransformListRow[]) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index 1fe20f1acae5a..192ff7ac74c57 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -6,7 +6,9 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TransformListAction, TransformListRow, TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + +import { TransformListAction, TransformListRow } from '../../../../common'; import { AuthorizationContext } from '../../../../lib/authorization'; import { editActionNameText, EditActionName } from './edit_action_name'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx index 191df0c16cba0..ca1c90b9b8fae 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { createCapabilityFailureMessage, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx index 8d6a4376c55b3..96af60778d6a4 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx @@ -6,7 +6,7 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; @@ -27,7 +27,7 @@ export const useStartAction = (forceDisable: boolean) => { const startAndCloseModal = () => { setModalVisible(false); - startTransforms(items); + startTransforms(items.map((i) => ({ id: i.id }))); }; const openModal = (newItems: TransformListRow[]) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx index e1ea82cb371e8..4ec30faa4d76b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListRow } from '../../../../common'; import { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx index e0a7e0b489ab6..4c872114a82ab 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; @@ -20,9 +20,10 @@ export const useStopAction = (forceDisable: boolean) => { const stopTransforms = useStopTransforms(); - const clickHandler = useCallback((item: TransformListRow) => stopTransforms([item]), [ - stopTransforms, - ]); + const clickHandler = useCallback( + (i: TransformListRow) => stopTransforms([{ id: i.id, state: i.stats.state }]), + [stopTransforms] + ); const action: TransformListAction = useMemo( () => ({ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index 735a059e57e14..f9cdac51b6582 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -23,13 +23,12 @@ import { EuiTitle, } from '@elastic/eui'; +import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + import { getErrorMessage } from '../../../../../../common/utils/errors'; -import { - refreshTransformList$, - TransformPivotConfig, - REFRESH_TRANSFORM_LIST_STATE, -} from '../../../../common'; +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common'; import { useToastNotifications } from '../../../../app_dependencies'; import { useApi } from '../../../../hooks/use_api'; @@ -58,19 +57,21 @@ export const EditTransformFlyout: FC = ({ closeFlyout, const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); const transformId = config.id; - try { - await api.updateTransform(transformId, requestConfig); - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', { - defaultMessage: 'Transform {transformId} updated.', - values: { transformId }, - }) - ); - closeFlyout(); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - } catch (e) { - setErrorMessage(getErrorMessage(e)); + const resp = await api.updateTransform(transformId, requestConfig); + + if (!isPostTransformsUpdateResponseSchema(resp)) { + setErrorMessage(getErrorMessage(resp)); + return; } + + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', { + defaultMessage: 'Transform {transformId} updated.', + values: { transformId }, + }) + ); + closeFlyout(); + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); } const isUpdateButtonDisabled = !state.isFormValid || !state.isFormTouched; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts index 4a8b26b601ae2..12e60c2af5556 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { applyFormFieldsToTransformConfig, @@ -86,9 +86,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }); test('should include previously nonexisting attributes', () => { - const transformConfigMock = getTransformConfigMock(); - delete transformConfigMock.description; - delete transformConfigMock.frequency; + const { description, frequency, ...transformConfigMock } = getTransformConfigMock(); const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, { description: getDescriptionFieldMock('the-new-description'), diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index 649db51e6ea78..d622a7e9cc040 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -9,7 +9,8 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { TransformPivotConfig } from '../../../../common'; +import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; // A Validator function takes in a value to check and returns an array of error messages. // If no messages (empty array) get returned, the value is valid. @@ -118,53 +119,35 @@ interface Action { value: string; } -// Some attributes can have a value of `null` to trigger -// a reset to the default value, or in the case of `docs_per_second` -// `null` is used to disable throttling. -interface UpdateTransformPivotConfig { - description: string; - frequency: string; - settings: { - docs_per_second: number | null; - }; -} - // Takes in the form configuration and returns a // request object suitable to be sent to the // transform update API endpoint. export const applyFormFieldsToTransformConfig = ( config: TransformPivotConfig, { description, docsPerSecond, frequency }: EditTransformFlyoutFieldsState -): Partial => { - const updateConfig: Partial = {}; - - // set the values only if they changed from the default - // and actually differ from the previous value. - if ( - !(config.frequency === undefined && frequency.value === '') && - config.frequency !== frequency.value - ) { - updateConfig.frequency = frequency.value; - } - - if ( - !(config.description === undefined && description.value === '') && - config.description !== description.value - ) { - updateConfig.description = description.value; - } - +): PostTransformsUpdateRequestSchema => { // if the input field was left empty, // fall back to the default value of `null` // which will disable throttling. const docsPerSecondFormValue = docsPerSecond.value !== '' ? parseInt(docsPerSecond.value, 10) : null; const docsPerSecondConfigValue = config.settings?.docs_per_second ?? null; - if (docsPerSecondFormValue !== docsPerSecondConfigValue) { - updateConfig.settings = { docs_per_second: docsPerSecondFormValue }; - } - return updateConfig; + return { + // set the values only if they changed from the default + // and actually differ from the previous value. + ...(!(config.frequency === undefined && frequency.value === '') && + config.frequency !== frequency.value + ? { frequency: frequency.value } + : {}), + ...(!(config.description === undefined && description.value === '') && + config.description !== description.value + ? { description: description.value } + : {}), + ...(docsPerSecondFormValue !== docsPerSecondConfigValue + ? { settings: { docs_per_second: docsPerSecondFormValue } } + : {}), + }; }; // Takes in a transform configuration and returns diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts index 11e4dc3dfa2b8..f6708f7c36f26 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import mockTransformListRow from '../../../../common/__mocks__/transform_list_row.json'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index 08545c288ba96..02bad50dc0dfd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -9,11 +9,15 @@ import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; -import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { i18n } from '@kbn/i18n'; + +import { isGetTransformsAuditMessagesResponseSchema } from '../../../../../../common/api_schemas/type_guards'; +import { TransformMessage } from '../../../../../../common/types/messages'; + import { useApi } from '../../../../hooks/use_api'; import { JobIcon } from '../../../../components/job_icon'; -import { TransformMessage } from '../../../../../../common/types/messages'; import { useRefreshTransformList } from '../../../../common'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -36,25 +40,16 @@ export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { let concurrentLoads = 0; return async function getMessages() { - try { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } + concurrentLoads++; - setIsLoading(true); - const messagesResp = await api.getTransformAuditMessages(transformId); - setIsLoading(false); - setMessages(messagesResp as any[]); + if (concurrentLoads > 1) { + return; + } - concurrentLoads--; + setIsLoading(true); + const messagesResp = await api.getTransformAuditMessages(transformId); - if (concurrentLoads > 0) { - concurrentLoads = 0; - getMessages(); - } - } catch (error) { + if (!isGetTransformsAuditMessagesResponseSchema(messagesResp)) { setIsLoading(false); setErrorMessage( i18n.translate( @@ -64,6 +59,17 @@ export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { } ) ); + return; + } + + setIsLoading(false); + setMessages(messagesResp as any[]); + + concurrentLoads--; + + if (concurrentLoads > 0) { + concurrentLoads = 0; + getMessages(); } }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index a917fc73ad8fb..87d9a25dababd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,10 +6,11 @@ import React, { useMemo, FC } from 'react'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { DataGrid } from '../../../../../shared_imports'; import { useToastNotifications } from '../../../../app_dependencies'; -import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { getPivotQuery } from '../../../../common'; import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index dad0f0e5ee282..12836c0a18ce2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -23,7 +23,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { TransformId } from '../../../../../../common'; +import { TransformId } from '../../../../../../common/types/transform'; import { useRefreshTransformList, @@ -189,7 +189,11 @@ export const TransformList: FC = ({
,
- stopTransforms(transformSelection)}> + + stopTransforms(transformSelection.map((t) => ({ id: t.id, state: t.stats.state }))) + } + >
, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx index fab591f881310..fdcb9ba5f0aff 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx @@ -16,8 +16,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TermClause, FieldClause, Value } from './common'; -import { TRANSFORM_STATE } from '../../../../../../common'; -import { TRANSFORM_MODE, TransformListRow } from '../../../../common'; +import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; +import { TransformListRow } from '../../../../common'; import { getTaskStateBadge } from './use_columns'; const filters: SearchFilterConfig[] = [ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx index bce01b954c83e..313668d4c5180 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx @@ -7,9 +7,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; -import { TRANSFORM_MODE, TransformListRow } from '../../../../common'; +import { TransformListRow } from '../../../../common'; import { StatsBar, TransformStatsBarStats } from '../stats_bar'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index d2d8c7084941d..040e502ce4888 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -22,24 +22,21 @@ import { RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; +import { TransformId } from '../../../../../../common/types/transform'; +import { TransformStats } from '../../../../../../common/types/transform_stats'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; -import { - getTransformProgress, - TransformListRow, - TransformStats, - TRANSFORM_LIST_COLUMN, -} from '../../../../common'; +import { getTransformProgress, TransformListRow, TRANSFORM_LIST_COLUMN } from '../../../../common'; import { useActions } from './use_actions'; -enum STATE_COLOR { - aborting = 'warning', - failed = 'danger', - indexing = 'primary', - started = 'primary', - stopped = 'hollow', - stopping = 'hollow', -} +const STATE_COLOR = { + aborting: 'warning', + failed: 'danger', + indexing: 'primary', + started: 'primary', + stopped: 'hollow', + stopping: 'hollow', +} as const; export const getTaskStateBadge = ( state: TransformStats['state'], diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 196df250b7a3d..4737787dbd9ee 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -27,7 +27,6 @@ export { DataGrid, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, diff --git a/x-pack/plugins/transform/server/README.md b/x-pack/plugins/transform/server/README.md new file mode 100644 index 0000000000000..1142c1fea094d --- /dev/null +++ b/x-pack/plugins/transform/server/README.md @@ -0,0 +1,19 @@ +# Transform Kibana API routes + +This folder contains Transform API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +There are custom parser and worker (`x-pack/plugins/transform/server/routes/apidoc_scripts`) to process api schemas for each documentation entry. It's written with typescript so make sure all the scripts in the folder are compiled before executing `apidoc` command. + +Make sure you have run `yarn kbn bootstrap` to get all requires dev dependencies. Then execute the following command from the transform plugin folder: +``` +yarn run apiDocs +``` +It compiles all the required scripts and generates the documentation both in HTML and Markdown formats. + + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `Transform_API.md` file. diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 5a479e4f429f6..269cd28c4bda6 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -10,11 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ResponseError, CustomHttpResponseOptions } from 'src/core/server'; -import { - TransformEndpointRequest, - TransformEndpointResult, - DeleteTransformEndpointResult, -} from '../../../common'; +import { CommonResponseStatusSchema, TransformIdsSchema } from '../../../common/api_schemas/common'; +import { DeleteTransformsResponseSchema } from '../../../common/api_schemas/delete_transforms'; const REQUEST_TIMEOUT = 'RequestTimeout'; @@ -23,9 +20,9 @@ export function isRequestTimeout(error: any) { } interface Params { - results: TransformEndpointResult | DeleteTransformEndpointResult; + results: CommonResponseStatusSchema | DeleteTransformsResponseSchema; id: string; - items: TransformEndpointRequest[]; + items: TransformIdsSchema; action: string; } @@ -63,7 +60,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) }, }; - const newResults: TransformEndpointResult | DeleteTransformEndpointResult = {}; + const newResults: CommonResponseStatusSchema | DeleteTransformsResponseSchema = {}; return items.reduce((accumResults, currentVal) => { if (results[currentVal.id] === undefined) { diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index 2642040c4cd0d..88352ec4af129 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -11,40 +11,49 @@ import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; +import { + indexPatternTitleSchema, + IndexPatternTitleSchema, +} from '../../../common/api_schemas/common'; +import { + fieldHistogramsRequestSchema, + FieldHistogramsRequestSchema, +} from '../../../common/api_schemas/field_histograms'; import { getHistogramsForFields } from '../../shared_imports'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { wrapError } from './error_utils'; -import { fieldHistogramsSchema, indexPatternTitleSchema, IndexPatternTitleSchema } from './schema'; export function registerFieldHistogramsRoutes({ router, license }: RouteDependencies) { - router.post( + router.post( { path: addBasePath('field_histograms/{indexPatternTitle}'), validate: { params: indexPatternTitleSchema, - body: fieldHistogramsSchema, + body: fieldHistogramsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { indexPatternTitle } = req.params as IndexPatternTitleSchema; - const { query, fields, samplerShardSize } = req.body; + license.guardApiRoute( + async (ctx, req, res) => { + const { indexPatternTitle } = req.params; + const { query, fields, samplerShardSize } = req.body; - try { - const resp = await getHistogramsForFields( - ctx.core.elasticsearch.client, - indexPatternTitle, - query, - fields, - samplerShardSize - ); + try { + const resp = await getHistogramsForFields( + ctx.core.elasticsearch.client, + indexPatternTitle, + query, + fields, + samplerShardSize + ); - return res.ok({ body: resp }); - } catch (e) { - return res.customError(wrapError(wrapEsError(e))); + return res.ok({ body: resp }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } } - }) + ) ); } diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts index 2b7b0544a8bf9..605cbde356fdf 100644 --- a/x-pack/plugins/transform/server/routes/api/privileges.ts +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES } from '../../../common/constants'; -import { Privileges } from '../../../common'; +import { Privileges } from '../../../common/types/privileges'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; diff --git a/x-pack/plugins/transform/server/routes/api/schema.ts b/x-pack/plugins/transform/server/routes/api/schema.ts deleted file mode 100644 index 8aadef81b221b..0000000000000 --- a/x-pack/plugins/transform/server/routes/api/schema.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { schema } from '@kbn/config-schema'; - -export const fieldHistogramsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** The fields to return histogram data. */ - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), -}); - -export const indexPatternTitleSchema = schema.object({ - /** Title of the index pattern for which to return stats. */ - indexPatternTitle: schema.string(), -}); - -export interface IndexPatternTitleSchema { - indexPatternTitle: string; -} - -export const schemaTransformId = { - params: schema.object({ - transformId: schema.string(), - }), -}; - -export interface SchemaTransformId { - transformId: string; -} - -export const deleteTransformSchema = schema.object({ - /** - * Delete Transform & Destination Index - */ - transformsInfo: schema.arrayOf( - schema.object({ - id: schema.string(), - state: schema.maybe(schema.string()), - }) - ), - deleteDestIndex: schema.maybe(schema.boolean()), - deleteDestIndexPattern: schema.maybe(schema.boolean()), - forceDelete: schema.maybe(schema.boolean()), -}); diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index efbe813db5e67..c02bc06ad6060 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -14,22 +14,47 @@ import { import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; +import { TRANSFORM_STATE } from '../../../common/constants'; +import { TransformId } from '../../../common/types/transform'; import { - TransformEndpointRequest, - TransformEndpointResult, - TransformId, - TRANSFORM_STATE, - DeleteTransformEndpointRequest, - DeleteTransformStatus, - ResultData, -} from '../../../common'; + transformIdParamSchema, + ResponseStatus, + TransformIdParamSchema, +} from '../../../common/api_schemas/common'; +import { + deleteTransformsRequestSchema, + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../common/api_schemas/delete_transforms'; +import { + startTransformsRequestSchema, + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../common/api_schemas/start_transforms'; +import { + stopTransformsRequestSchema, + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../common/api_schemas/stop_transforms'; +import { + postTransformsUpdateRequestSchema, + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../common/api_schemas/update_transforms'; +import { + GetTransformsResponseSchema, + postTransformsPreviewRequestSchema, + PostTransformsPreviewRequestSchema, + putTransformsRequestSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../common/api_schemas/transforms'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils'; -import { deleteTransformSchema, schemaTransformId, SchemaTransformId } from './schema'; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; @@ -47,6 +72,16 @@ interface StopOptions { export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const { router, license } = routeDependencies; + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms Get transforms + * @apiName GetTransforms + * @apiDescription Returns transforms + * + * @apiSchema (params) jobAuditMessagesJobIdSchema + * @apiSchema (query) jobAuditMessagesQuerySchema + */ router.get( { path: addBasePath('transforms'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -62,16 +97,24 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); - router.get( + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/:transformId Get transform + * @apiName GetTransform + * @apiDescription Returns a single transform + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( { path: addBasePath('transforms/{transformId}'), - validate: schemaTransformId, + validate: { params: transformIdParamSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; - const options = { - ...(transformId !== undefined ? { transformId } : {}), - }; + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; + const options = transformId !== undefined ? { transformId } : {}; try { const transforms = await getTransforms( options, @@ -83,6 +126,14 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/_stats Get transforms stats + * @apiName GetTransformsStats + * @apiDescription Returns transforms stats + */ router.get( { path: addBasePath('transforms/_stats'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -98,13 +149,23 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); - router.get( + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/:transformId/_stats Get transform stats + * @apiName GetTransformStats + * @apiDescription Returns stats for a single transform + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( { path: addBasePath('transforms/{transformId}/_stats'), - validate: schemaTransformId, + validate: { params: transformIdParamSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; const options = { ...(transformId !== undefined ? { transformId } : {}), }; @@ -120,134 +181,198 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { }) ); registerTransformsAuditMessagesRoutes(routeDependencies); - router.put( + + /** + * @apiGroup Transforms + * + * @api {put} /api/transform/transforms/:transformId Put transform + * @apiName PutTransform + * @apiDescription Creates a transform + * + * @apiSchema (params) transformIdParamSchema + * @apiSchema (body) putTransformsRequestSchema + */ + router.put( { path: addBasePath('transforms/{transformId}'), validate: { - ...schemaTransformId, - body: schema.maybe(schema.any()), + params: transformIdParamSchema, + body: putTransformsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; - - const response: { - transformsCreated: Array<{ transform: string }>; - errors: any[]; - } = { - transformsCreated: [], - errors: [], - }; + license.guardApiRoute( + async (ctx, req, res) => { + const { transformId } = req.params; - await ctx - .transform!.dataClient.callAsCurrentUser('transform.createTransform', { - body: req.body, - transformId, - }) - .then(() => response.transformsCreated.push({ transform: transformId })) - .catch((e) => - response.errors.push({ - id: transformId, - error: wrapEsError(e), + const response: PutTransformsResponseSchema = { + transformsCreated: [], + errors: [], + }; + + await ctx + .transform!.dataClient.callAsCurrentUser('transform.createTransform', { + body: req.body, + transformId, }) - ); + .then(() => response.transformsCreated.push({ transform: transformId })) + .catch((e) => + response.errors.push({ + id: transformId, + error: wrapEsError(e), + }) + ); - return res.ok({ body: response }); - }) + return res.ok({ body: response }); + } + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/transforms/:transformId/_update Post transform update + * @apiName PostTransformUpdate + * @apiDescription Updates a transform + * + * @apiSchema (params) transformIdParamSchema + * @apiSchema (body) postTransformsUpdateRequestSchema + */ + router.post( { path: addBasePath('transforms/{transformId}/_update'), validate: { - ...schemaTransformId, - body: schema.maybe(schema.any()), + params: transformIdParamSchema, + body: postTransformsUpdateRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + license.guardApiRoute( + async (ctx, req, res) => { + const { transformId } = req.params; - try { - return res.ok({ - body: await ctx.transform!.dataClient.callAsCurrentUser('transform.updateTransform', { - body: req.body, - transformId, - }), - }); - } catch (e) { - return res.customError(wrapError(e)); + try { + return res.ok({ + body: (await ctx.transform!.dataClient.callAsCurrentUser('transform.updateTransform', { + body: req.body, + transformId, + })) as PostTransformsUpdateResponseSchema, + }); + } catch (e) { + return res.customError(wrapError(e)); + } } - }) + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/delete_transforms Post delete transforms + * @apiName DeleteTransforms + * @apiDescription Deletes transforms + * + * @apiSchema (body) deleteTransformsRequestSchema + */ + router.post( { path: addBasePath('delete_transforms'), validate: { - body: deleteTransformSchema, + body: deleteTransformsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - } = req.body as DeleteTransformEndpointRequest; - - try { - const body = await deleteTransforms( - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - ctx, - license, - res - ); - - if (body && body.status) { - if (body.status === 404) { - return res.notFound(); - } - if (body.status === 403) { - return res.forbidden(); + license.guardApiRoute( + async (ctx, req, res) => { + try { + const body = await deleteTransforms(req.body, ctx, res); + + if (body && body.status) { + if (body.status === 404) { + return res.notFound(); + } + if (body.status === 403) { + return res.forbidden(); + } } - } - return res.ok({ - body, - }); - } catch (e) { - return res.customError(wrapError(wrapEsError(e))); + return res.ok({ + body, + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } } - }) + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/transforms/_preview Preview transform + * @apiName PreviewTransform + * @apiDescription Previews transform + * + * @apiSchema (body) postTransformsPreviewRequestSchema + */ + router.post( { path: addBasePath('transforms/_preview'), validate: { - body: schema.maybe(schema.any()), + body: postTransformsPreviewRequestSchema, }, }, - license.guardApiRoute(previewTransformHandler) + license.guardApiRoute( + previewTransformHandler + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/start_transforms Start transforms + * @apiName PostStartTransforms + * @apiDescription Starts transform + * + * @apiSchema (body) startTransformsRequestSchema + */ + router.post( { path: addBasePath('start_transforms'), validate: { - body: schema.maybe(schema.any()), + body: startTransformsRequestSchema, }, }, - license.guardApiRoute(startTransformsHandler) + license.guardApiRoute( + startTransformsHandler + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/stop_transforms Stop transforms + * @apiName PostStopTransforms + * @apiDescription Stops transform + * + * @apiSchema (body) stopTransformsRequestSchema + */ + router.post( { path: addBasePath('stop_transforms'), validate: { - body: schema.maybe(schema.any()), + body: stopTransformsRequestSchema, }, }, - license.guardApiRoute(stopTransformsHandler) + license.guardApiRoute(stopTransformsHandler) ); + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/es_search Transform ES Search Proxy + * @apiName PostTransformEsSearchProxy + * @apiDescription ES Search Proxy + * + * @apiSchema (body) any + */ router.post( { path: addBasePath('es_search'), @@ -267,7 +392,10 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { ); } -const getTransforms = async (options: { transformId?: string }, callAsCurrentUser: CallCluster) => { +const getTransforms = async ( + options: { transformId?: string }, + callAsCurrentUser: CallCluster +): Promise => { return await callAsCurrentUser('transform.getTransforms', options); }; @@ -294,22 +422,25 @@ async function deleteDestIndexPatternById( } async function deleteTransforms( - transformsInfo: TransformEndpointRequest[], - deleteDestIndex: boolean | undefined, - deleteDestIndexPattern: boolean | undefined, - shouldForceDelete: boolean = false, + reqBody: DeleteTransformsRequestSchema, ctx: RequestHandlerContext, - license: RouteDependencies['license'], response: KibanaResponseFactory ) { - const results: Record = {}; + const { transformsInfo } = reqBody; + + // Cast possible undefineds as booleans + const deleteDestIndex = !!reqBody.deleteDestIndex; + const deleteDestIndexPattern = !!reqBody.deleteDestIndexPattern; + const shouldForceDelete = !!reqBody.forceDelete; + + const results: DeleteTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { let destinationIndex: string | undefined; - const transformDeleted: ResultData = { success: false }; - const destIndexDeleted: ResultData = { success: false }; - const destIndexPatternDeleted: ResultData = { + const transformDeleted: ResponseStatus = { success: false }; + const destIndexDeleted: ResponseStatus = { success: false }; + const destIndexPatternDeleted: ResponseStatus = { success: false, }; const transformId = transformInfo.id; @@ -405,7 +536,11 @@ async function deleteTransforms( return results; } -const previewTransformHandler: RequestHandler = async (ctx, req, res) => { +const previewTransformHandler: RequestHandler< + undefined, + undefined, + PostTransformsPreviewRequestSchema +> = async (ctx, req, res) => { try { return res.ok({ body: await ctx.transform!.dataClient.callAsCurrentUser('transform.getTransformsPreview', { @@ -417,8 +552,12 @@ const previewTransformHandler: RequestHandler = async (ctx, req, res) => { } }; -const startTransformsHandler: RequestHandler = async (ctx, req, res) => { - const transformsInfo = req.body as TransformEndpointRequest[]; +const startTransformsHandler: RequestHandler< + undefined, + undefined, + StartTransformsRequestSchema +> = async (ctx, req, res) => { + const transformsInfo = req.body; try { return res.ok({ @@ -430,15 +569,15 @@ const startTransformsHandler: RequestHandler = async (ctx, req, res) => { }; async function startTransforms( - transformsInfo: TransformEndpointRequest[], + transformsInfo: StartTransformsRequestSchema, callAsCurrentUser: CallCluster ) { - const results: TransformEndpointResult = {}; + const results: StartTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; try { - await callAsCurrentUser('transform.startTransform', { transformId } as SchemaTransformId); + await callAsCurrentUser('transform.startTransform', { transformId }); results[transformId] = { success: true }; } catch (e) { if (isRequestTimeout(e)) { @@ -455,8 +594,12 @@ async function startTransforms( return results; } -const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { - const transformsInfo = req.body as TransformEndpointRequest[]; +const stopTransformsHandler: RequestHandler< + undefined, + undefined, + StopTransformsRequestSchema +> = async (ctx, req, res) => { + const transformsInfo = req.body; try { return res.ok({ @@ -468,10 +611,10 @@ const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { }; async function stopTransforms( - transformsInfo: TransformEndpointRequest[], + transformsInfo: StopTransformsRequestSchema, callAsCurrentUser: CallCluster ) { - const results: TransformEndpointResult = {}; + const results: StopTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index 722a3f52376b4..f01b2bdb73fd5 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessage } from '../../../common/types/messages'; +import { transformIdParamSchema, TransformIdParamSchema } from '../../../common/api_schemas/common'; +import { AuditMessage, TransformMessage } from '../../../common/types/messages'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { RouteDependencies } from '../../types'; @@ -12,7 +13,6 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { wrapError } from './error_utils'; -import { schemaTransformId, SchemaTransformId } from './schema'; const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; const SIZE = 500; @@ -22,10 +22,22 @@ interface BoolQuery { } export function registerTransformsAuditMessagesRoutes({ router, license }: RouteDependencies) { - router.get( - { path: addBasePath('transforms/{transformId}/messages'), validate: schemaTransformId }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + /** + * @apiGroup Transforms Audit Messages + * + * @api {get} /api/transform/transforms/:transformId/messages Transforms Messages + * @apiName GetTransformsMessages + * @apiDescription Get transforms audit messages + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( + { + path: addBasePath('transforms/{transformId}/messages'), + validate: { params: transformIdParamSchema }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; // search for audit messages, // transformId is optional. without it, all transforms will be listed. @@ -77,7 +89,7 @@ export function registerTransformsAuditMessagesRoutes({ router, license }: Route }, }); - let messages = []; + let messages: TransformMessage[] = []; if (resp.hits.total !== 0) { messages = resp.hits.hits.map((hit: AuditMessage) => hit._source); messages.reverse(); diff --git a/x-pack/plugins/transform/server/routes/apidoc.json b/x-pack/plugins/transform/server/routes/apidoc.json new file mode 100644 index 0000000000000..ce76b5b302f93 --- /dev/null +++ b/x-pack/plugins/transform/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "transform_kibana_api", + "version": "7.10.0", + "description": "This is the documentation of the REST API provided by the Transform Kibana plugin. Each API is experimental and can include breaking changes in any version.", + "title": "Transform Kibana API", + "order": [ + "GetTransforms", + "GetTransform", + "GetTransformsStats", + "GetTransformStats", + "PutTransform", + "PostTransformUpdate", + "DeleteTransforms", + "PreviewTransform", + "PostStartTransforms", + "PostStopTransforms", + "PostTransformEsSearchProxy", + "DeleteDataFrameAnalytics", + "GetTransformsMessages" + ] +} diff --git a/x-pack/plugins/transform/server/services/license.ts b/x-pack/plugins/transform/server/services/license.ts index 1a2768999fdc4..bacf9724a6253 100644 --- a/x-pack/plugins/transform/server/services/license.ts +++ b/x-pack/plugins/transform/server/services/license.ts @@ -62,12 +62,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ): IKibanaResponse | Promise> { const licenseStatus = license.getStatus(); diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json new file mode 100644 index 0000000000000..6f83eb665f830 --- /dev/null +++ b/x-pack/plugins/transform/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json", +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c014c61903935..2f0e264a81e91 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4861,12 +4861,9 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "アップグレードアシスタント", "xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。", - "xpack.apm.servicesTable.agentColumnLabel": "エージェント", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "1 分あたりのエラー", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "エラー", "xpack.apm.servicesTable.nameColumnLabel": "名前", "xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!", "xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません", @@ -9704,7 +9701,6 @@ "xpack.lens.pieChart.legendDisplayLegend": "凡例表示", "xpack.lens.pieChart.nestedLegendLabel": "ネストされた凡例", "xpack.lens.pieChart.numberLabels": "ラベル値", - "xpack.lens.pieChart.percentDecimalsLabel": "割合の小数点桁数", "xpack.lens.pieChart.showCategoriesLabel": "内部または外部", "xpack.lens.pieChart.showFormatterValuesLabel": "値を表示", "xpack.lens.pieChart.showPercentValuesLabel": "割合を表示", @@ -13709,7 +13705,6 @@ "xpack.monitoring.updateLicenseTitle": "ライセンスの更新", "xpack.monitoring.useAvailableLicenseDescription": "既に新しいライセンスがある場合は、今すぐアップロードしてください。", "xpack.monitoring.wedLabel": "水", - "xpack.observability.beta": "ベータ", "xpack.observability.emptySection.apps.alert.description": "503エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 96bd112ca5e02..d3ae5124b0f71 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4864,12 +4864,9 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "升级助手", "xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。", - "xpack.apm.servicesTable.agentColumnLabel": "代理", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "每分钟错误数", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "错误", "xpack.apm.servicesTable.nameColumnLabel": "名称", "xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!", "xpack.apm.servicesTable.notFoundLabel": "未找到任何服务", @@ -9710,7 +9707,6 @@ "xpack.lens.pieChart.legendDisplayLegend": "图例显示", "xpack.lens.pieChart.nestedLegendLabel": "嵌套图例", "xpack.lens.pieChart.numberLabels": "标签值", - "xpack.lens.pieChart.percentDecimalsLabel": "百分比的小数位数", "xpack.lens.pieChart.showCategoriesLabel": "内部或外部", "xpack.lens.pieChart.showFormatterValuesLabel": "显示值", "xpack.lens.pieChart.showPercentValuesLabel": "显示百分比", @@ -13718,7 +13714,6 @@ "xpack.monitoring.updateLicenseTitle": "更新您的许可证", "xpack.monitoring.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。", "xpack.monitoring.wedLabel": "周三", - "xpack.observability.beta": "公测版", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", diff --git a/x-pack/run_functional_tests.sh b/x-pack/run_functional_tests.sh deleted file mode 100755 index e94f283ea0394..0000000000000 --- a/x-pack/run_functional_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -export TEST_KIBANA_URL="http://elastic:mlqa_admin@localhost:5601" -export TEST_ES_URL="http://elastic:mlqa_admin@localhost:9200" -node ../scripts/functional_test_runner --include-tag walterra diff --git a/x-pack/test/api_integration/apis/transform/common.ts b/x-pack/test/api_integration/apis/transform/common.ts new file mode 100644 index 0000000000000..1a48ee987bc77 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/common.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; + +export async function asyncForEach(array: any[], callback: Function) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} + +export function generateDestIndex(transformId: string): string { + return `user-${transformId}`; +} + +export function generateTransformConfig(transformId: string): PutTransformsRequestSchema { + const destinationIndex = generateDestIndex(transformId); + + return { + source: { index: ['ft_farequote'] }, + pivot: { + group_by: { airline: { terms: { field: 'airline' } } }, + aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } }, + }, + dest: { index: destinationIndex }, + }; +} diff --git a/x-pack/test/api_integration/apis/transform/delete_transforms.ts b/x-pack/test/api_integration/apis/transform/delete_transforms.ts index 7f01d2741ad15..41b2bffb1f0ad 100644 --- a/x-pack/test/api_integration/apis/transform/delete_transforms.ts +++ b/x-pack/test/api_integration/apis/transform/delete_transforms.ts @@ -4,41 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { TransformEndpointRequest } from '../../../../plugins/transform/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import { DeleteTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/delete_transforms'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; import { USER } from '../../../functional/services/transform/security_common'; -async function asyncForEach(array: any[], callback: Function) { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array); - } -} +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); const transform = getService('transform'); - function generateDestIndex(transformId: string): string { - return `user-${transformId}`; + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); } - async function createTransform(transformId: string, destinationIndex: string) { - const config = { - id: transformId, - source: { index: ['farequote-*'] }, - pivot: { - group_by: { airline: { terms: { field: 'airline' } } }, - aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } }, - }, - dest: { index: destinationIndex }, - }; - - await transform.api.createTransform(config); - } - - describe('delete_transforms', function () { + describe('/api/transform/delete_transforms', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await transform.testResources.setKibanaTimeZoneToUTC(); @@ -49,11 +36,11 @@ export default ({ getService }: FtrProviderContext) => { }); describe('single transform deletion', function () { - const transformId = 'test1'; + const transformId = 'transform-test-delete'; const destinationIndex = generateDestIndex(transformId); beforeEach(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); }); @@ -62,7 +49,9 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform by transformId', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -70,9 +59,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -83,7 +70,9 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 403 for unauthorized user', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -91,9 +80,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(403); await transform.api.waitForTransformToExist(transformId); await transform.api.waitForIndicesToExist(destinationIndex); @@ -102,7 +89,9 @@ export default ({ getService }: FtrProviderContext) => { describe('single transform deletion with invalid transformId', function () { it('should return 200 with error in response if invalid transformId', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: 'invalid_transform_id' }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: 'invalid_transform_id', state: TRANSFORM_STATE.STOPPED }], + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -110,9 +99,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); expect(body.invalid_transform_id.transformDeleted.success).to.eql(false); expect(body.invalid_transform_id.transformDeleted).to.have.property('error'); @@ -120,15 +107,17 @@ export default ({ getService }: FtrProviderContext) => { }); describe('bulk deletion', function () { - const transformsInfo: TransformEndpointRequest[] = [ - { id: 'bulk_delete_test_1' }, - { id: 'bulk_delete_test_2' }, - ]; - const destinationIndices = transformsInfo.map((d) => generateDestIndex(d.id)); + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [ + { id: 'bulk_delete_test_1', state: TRANSFORM_STATE.STOPPED }, + { id: 'bulk_delete_test_2', state: TRANSFORM_STATE.STOPPED }, + ], + }; + const destinationIndices = reqBody.transformsInfo.map((d) => generateDestIndex(d.id)); beforeEach(async () => { - await asyncForEach(transformsInfo, async ({ id }: { id: string }, idx: number) => { - await createTransform(id, destinationIndices[idx]); + await asyncForEach(reqBody.transformsInfo, async ({ id }: { id: string }, idx: number) => { + await createTransform(id); await transform.api.createIndices(destinationIndices[idx]); }); }); @@ -147,13 +136,11 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); await asyncForEach( - transformsInfo, + reqBody.transformsInfo, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].transformDeleted.success).to.eql(true); expect(body[transformId].destIndexDeleted.success).to.eql(false); @@ -174,16 +161,16 @@ export default ({ getService }: FtrProviderContext) => { ) .set(COMMON_REQUEST_HEADERS) .send({ + ...reqBody, transformsInfo: [ - { id: transformsInfo[0].id }, - { id: invalidTransformId }, - { id: transformsInfo[1].id }, + ...reqBody.transformsInfo, + { id: invalidTransformId, state: TRANSFORM_STATE.STOPPED }, ], }) .expect(200); await asyncForEach( - transformsInfo, + reqBody.transformsInfo, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].transformDeleted.success).to.eql(true); expect(body[transformId].destIndexDeleted.success).to.eql(false); @@ -203,7 +190,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); }); @@ -212,7 +199,10 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform and destination index', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -220,10 +210,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -239,7 +226,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); await transform.testResources.createIndexPatternIfNeeded(destinationIndex); }); @@ -250,7 +237,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform and destination index pattern', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: false, + deleteDestIndexPattern: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -258,11 +249,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: false, - deleteDestIndexPattern: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -279,7 +266,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); await transform.testResources.createIndexPatternIfNeeded(destinationIndex); }); @@ -290,7 +277,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform, destination index, & destination index pattern', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: true, + deleteDestIndexPattern: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -298,11 +289,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: true, - deleteDestIndexPattern: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); diff --git a/x-pack/test/api_integration/apis/transform/index.ts b/x-pack/test/api_integration/apis/transform/index.ts index 93a951a55ece1..ef08883534d10 100644 --- a/x-pack/test/api_integration/apis/transform/index.ts +++ b/x-pack/test/api_integration/apis/transform/index.ts @@ -28,5 +28,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./delete_transforms')); + loadTestFile(require.resolve('./start_transforms')); + loadTestFile(require.resolve('./stop_transforms')); + loadTestFile(require.resolve('./transforms')); + loadTestFile(require.resolve('./transforms_preview')); + loadTestFile(require.resolve('./transforms_stats')); + loadTestFile(require.resolve('./transforms_update')); }); } diff --git a/x-pack/test/api_integration/apis/transform/start_transforms.ts b/x-pack/test/api_integration/apis/transform/start_transforms.ts new file mode 100644 index 0000000000000..288a3caae390e --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/start_transforms.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { StartTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/start_transforms'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + describe('/api/transform/start_transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('single transform start', function () { + const transformId = 'transform-test-start'; + const destinationIndex = generateDestIndex(transformId); + + beforeEach(async () => { + await createTransform(transformId); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await transform.api.deleteIndices(destinationIndex); + }); + + it('should start the transform by transformId', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: transformId }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body[transformId].success).to.eql(true); + expect(typeof body[transformId].error).to.eql('undefined'); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + + it('should return 200 with success:false for unauthorized user', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: transformId }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body[transformId].success).to.eql(false); + expect(typeof body[transformId].error).to.eql('string'); + + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesNotToExist(destinationIndex); + }); + }); + + describe('single transform start with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: 'invalid_transform_id' }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body.invalid_transform_id.success).to.eql(false); + expect(body.invalid_transform_id).to.have.property('error'); + }); + }); + + describe('bulk start', function () { + const reqBody: StartTransformsRequestSchema = [ + { id: 'bulk_start_test_1' }, + { id: 'bulk_start_test_2' }, + ]; + const destinationIndices = reqBody.map((d) => generateDestIndex(d.id)); + + beforeEach(async () => { + await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => { + await createTransform(id); + }); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await asyncForEach(destinationIndices, async (destinationIndex: string) => { + await transform.api.deleteIndices(destinationIndex); + }); + }); + + it('should start multiple transforms by transformIds', async () => { + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + }); + + it('should start multiple transforms by transformIds, even if one of the transformIds is invalid', async () => { + const invalidTransformId = 'invalid_transform_id'; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send([{ id: reqBody[0].id }, { id: invalidTransformId }, { id: reqBody[1].id }]) + .expect(200); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + + expect(body[invalidTransformId].success).to.eql(false); + expect(body[invalidTransformId]).to.have.property('error'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/stop_transforms.ts b/x-pack/test/api_integration/apis/transform/stop_transforms.ts new file mode 100644 index 0000000000000..4f30db0794ea4 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/stop_transforms.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import type { StopTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/stop_transforms'; +import { isStopTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; + +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + async function createAndRunTransform(transformId: string) { + // to be able to test stopping transforms, + // we create a slow continuous transform + // so it doesn't stop automatically. + const config: PutTransformsRequestSchema = { + ...generateTransformConfig(transformId), + settings: { + docs_per_second: 10, + max_page_search_size: 10, + }, + sync: { + time: { field: '@timestamp' }, + }, + }; + + await transform.api.createAndRunTransform(transformId, config); + } + + describe('/api/transform/stop_transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('single transform stop', function () { + const transformId = 'transform-test-stop'; + const destinationIndex = generateDestIndex(transformId); + + beforeEach(async () => { + await createAndRunTransform(transformId); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await transform.api.deleteIndices(destinationIndex); + }); + + it('should stop the transform by transformId', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: transformId, state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body[transformId].success).to.eql(true); + expect(typeof body[transformId].error).to.eql('undefined'); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + + it('should return 200 with success:false for unauthorized user', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: transformId, state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body[transformId].success).to.eql(false); + expect(typeof body[transformId].error).to.eql('string'); + + await transform.api.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + }); + + describe('single transform stop with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: 'invalid_transform_id', state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body.invalid_transform_id.success).to.eql(false); + expect(body.invalid_transform_id).to.have.property('error'); + }); + }); + + describe('bulk stop', function () { + const reqBody: StopTransformsRequestSchema = [ + { id: 'bulk_stop_test_1', state: TRANSFORM_STATE.STARTED }, + { id: 'bulk_stop_test_2', state: TRANSFORM_STATE.STARTED }, + ]; + const destinationIndices = reqBody.map((d) => generateDestIndex(d.id)); + + beforeEach(async () => { + await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => { + await createAndRunTransform(id); + }); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await asyncForEach(destinationIndices, async (destinationIndex: string) => { + await transform.api.deleteIndices(destinationIndex); + }); + }); + + it('should stop multiple transforms by transformIds', async () => { + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + }); + + it('should stop multiple transforms by transformIds, even if one of the transformIds is invalid', async () => { + const invalidTransformId = 'invalid_transform_id'; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send([ + { id: reqBody[0].id, state: reqBody[0].state }, + { id: invalidTransformId, state: TRANSFORM_STATE.STOPPED }, + { id: reqBody[1].id, state: reqBody[1].state }, + ]) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + + expect(body[invalidTransformId].success).to.eql(false); + expect(body[invalidTransformId]).to.have.property('error'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms.ts b/x-pack/test/api_integration/apis/transform/transforms.ts new file mode 100644 index 0000000000000..c44c2b58e6207 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { GetTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import { isGetTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransforms: { + count: 2, + transform1: { id: 'transform-test-get-1', destIndex: 'user-transform-test-get-1' }, + transform2: { id: 'transform-test-get-2', destIndex: 'user-transform-test-get-2' }, + typeOfVersion: 'string', + typeOfCreateTime: 'number', + }, + apiTransformTransformsTransformId: { + count: 1, + transform1: { id: 'transform-test-get-1', destIndex: 'user-transform-test-get-1' }, + typeOfVersion: 'string', + typeOfCreateTime: 'number', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function assertTransformsResponseBody(body: GetTransformsResponseSchema) { + expect(isGetTransformsResponseSchema(body)).to.eql(true); + + expect(body.count).to.eql(expected.apiTransformTransforms.count); + expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransforms.transform1.id); + expect(transform1.dest.index).to.eql(expected.apiTransformTransforms.transform1.destIndex); + expect(typeof transform1.version).to.eql(expected.apiTransformTransforms.typeOfVersion); + expect(typeof transform1.create_time).to.eql(expected.apiTransformTransforms.typeOfCreateTime); + + const transform2 = body.transforms[1]; + expect(transform2.id).to.eql(expected.apiTransformTransforms.transform2.id); + expect(transform2.dest.index).to.eql(expected.apiTransformTransforms.transform2.destIndex); + expect(typeof transform2.version).to.eql(expected.apiTransformTransforms.typeOfVersion); + expect(typeof transform2.create_time).to.eql(expected.apiTransformTransforms.typeOfCreateTime); + } + + function assertSingleTransformResponseBody(body: GetTransformsResponseSchema) { + expect(isGetTransformsResponseSchema(body)).to.eql(true); + + expect(body.count).to.eql(expected.apiTransformTransformsTransformId.count); + expect(body.transforms).to.have.length(expected.apiTransformTransformsTransformId.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransformsTransformId.transform1.id); + expect(transform1.dest.index).to.eql( + expected.apiTransformTransformsTransformId.transform1.destIndex + ); + expect(typeof transform1.version).to.eql( + expected.apiTransformTransformsTransformId.typeOfVersion + ); + expect(typeof transform1.create_time).to.eql( + expected.apiTransformTransformsTransformId.typeOfCreateTime + ); + } + + describe('/api/transform/transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-get-1'); + await createTransform('transform-test-get-2'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + describe('/transforms', function () { + it('should return a list of transforms for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsResponseBody(body); + }); + + it('should return a list of transforms for transform view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsResponseBody(body); + }); + }); + + describe('/transforms/{transformId}', function () { + it('should return a specific transform configuration for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms/transform-test-get-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertSingleTransformResponseBody(body); + }); + + it('should return a specific transform configuration transform view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms/transform-test-get-1`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertSingleTransformResponseBody(body); + }); + + it('should report 404 for a non-existing transform', async () => { + await supertest + .get('/api/transform/transforms/the-non-existing-transform') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(404); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_preview.ts b/x-pack/test/api_integration/apis/transform/transforms_preview.ts new file mode 100644 index 0000000000000..d0fc44cf28fdb --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_preview.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { PostTransformsPreviewRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransformsPreview: { + previewItemCount: 19, + typeOfGeneratedDestIndex: 'object', + }, + }; + + function getTransformPreviewConfig() { + // passing in an empty string for transform id since we will not use + // it as part of the config request schema. Destructuring will + // remove the `dest` part of the config. + const { dest, ...config } = generateTransformConfig(''); + return config as PostTransformsPreviewRequestSchema; + } + + describe('/api/transform/transforms/_preview', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.api.waitForIndicesToExist('ft_farequote'); + }); + + it('should return a transform preview', async () => { + const { body } = await supertest + .post('/api/transform/transforms/_preview') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformPreviewConfig()) + .expect(200); + + expect(body.preview).to.have.length(expected.apiTransformTransformsPreview.previewItemCount); + expect(typeof body.generated_dest_index).to.eql( + expected.apiTransformTransformsPreview.typeOfGeneratedDestIndex + ); + }); + + it('should return 403 for transform view-only user', async () => { + await supertest + .post(`/api/transform/transforms/_preview`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformPreviewConfig()) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_stats.ts b/x-pack/test/api_integration/apis/transform/transforms_stats.ts new file mode 100644 index 0000000000000..07856e5095a98 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_stats.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { GetTransformsStatsResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms_stats'; +import { isGetTransformsStatsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransforms: { + count: 2, + transform1: { id: 'transform-test-stats-1', state: TRANSFORM_STATE.STOPPED }, + transform2: { id: 'transform-test-stats-2', state: TRANSFORM_STATE.STOPPED }, + typeOfStats: 'object', + typeOfCheckpointing: 'object', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function assertTransformsStatsResponseBody(body: GetTransformsStatsResponseSchema) { + expect(isGetTransformsStatsResponseSchema(body)).to.eql(true); + expect(body.count).to.eql(expected.apiTransformTransforms.count); + expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransforms.transform1.id); + expect(transform1.state).to.eql(expected.apiTransformTransforms.transform1.state); + expect(typeof transform1.stats).to.eql(expected.apiTransformTransforms.typeOfStats); + expect(typeof transform1.checkpointing).to.eql( + expected.apiTransformTransforms.typeOfCheckpointing + ); + + const transform2 = body.transforms[1]; + expect(transform2.id).to.eql(expected.apiTransformTransforms.transform2.id); + expect(transform2.state).to.eql(expected.apiTransformTransforms.transform2.state); + expect(typeof transform2.stats).to.eql(expected.apiTransformTransforms.typeOfStats); + expect(typeof transform2.checkpointing).to.eql( + expected.apiTransformTransforms.typeOfCheckpointing + ); + } + + describe('/api/transform/transforms/_stats', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-stats-1'); + await createTransform('transform-test-stats-2'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should return a list of transforms statistics for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms/_stats') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsStatsResponseBody(body); + }); + + it('should return a list of transforms statistics view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms/_stats`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsStatsResponseBody(body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_update.ts b/x-pack/test/api_integration/apis/transform/transforms_update.ts new file mode 100644 index 0000000000000..3ad5b5b47c79b --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_update.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + transformOriginalConfig: { + count: 1, + id: 'transform-test-update-1', + source: { + index: ['ft_farequote'], + query: { match_all: {} }, + }, + }, + apiTransformTransformsPreview: { + previewItemCount: 19, + typeOfGeneratedDestIndex: 'object', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function getTransformUpdateConfig() { + return { + source: { + index: 'ft_*', + query: { + term: { + airline: { + value: 'AAL', + }, + }, + }, + }, + description: 'the-updated-description', + dest: { + index: 'user-the-updated-destination-index', + }, + frequency: '60m', + }; + } + + describe('/api/transform/transforms/{transformId}/_update', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-update-1'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should update a transform', async () => { + // assert the original transform for comparison + const { body: transformOriginalBody } = await supertest + .get('/api/transform/transforms/transform-test-update-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + expect(transformOriginalBody.count).to.eql(expected.transformOriginalConfig.count); + expect(transformOriginalBody.transforms).to.have.length( + expected.transformOriginalConfig.count + ); + + const transformOriginalConfig = transformOriginalBody.transforms[0]; + expect(transformOriginalConfig.id).to.eql(expected.transformOriginalConfig.id); + expect(transformOriginalConfig.source).to.eql(expected.transformOriginalConfig.source); + expect(transformOriginalConfig.description).to.eql(undefined); + expect(transformOriginalConfig.settings).to.eql({}); + + // update the transform and assert the response + const { body: transformUpdateResponseBody } = await supertest + .post('/api/transform/transforms/transform-test-update-1/_update') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformUpdateConfig()) + .expect(200); + + const expectedUpdateConfig = getTransformUpdateConfig(); + expect(transformUpdateResponseBody.id).to.eql(expected.transformOriginalConfig.id); + expect(transformUpdateResponseBody.source).to.eql({ + ...expectedUpdateConfig.source, + index: ['ft_*'], + }); + expect(transformUpdateResponseBody.description).to.eql(expectedUpdateConfig.description); + expect(transformUpdateResponseBody.settings).to.eql({}); + + // assert the updated transform for comparison + const { body: transformUpdatedBody } = await supertest + .get('/api/transform/transforms/transform-test-update-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + expect(transformUpdatedBody.count).to.eql(expected.transformOriginalConfig.count); + expect(transformUpdatedBody.transforms).to.have.length( + expected.transformOriginalConfig.count + ); + + const transformUpdatedConfig = transformUpdatedBody.transforms[0]; + expect(transformUpdatedConfig.id).to.eql(expected.transformOriginalConfig.id); + expect(transformUpdatedConfig.source).to.eql({ + ...expectedUpdateConfig.source, + index: ['ft_*'], + }); + expect(transformUpdatedConfig.description).to.eql(expectedUpdateConfig.description); + expect(transformUpdatedConfig.settings).to.eql({}); + }); + + it('should return 403 for transform view-only user', async () => { + await supertest + .post('/api/transform/transforms/transform-test-update-1/_update') + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformUpdateConfig()) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts index 127721e8e2112..6d0d2d3042625 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts @@ -10,6 +10,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + describe('Has data', () => { describe('when data is not loaded', () => { it('returns false when there is no data', async () => { @@ -28,8 +30,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns true when there is at least one document on transaction, error or metrics indices', async () => { const response = await supertest.get('/api/apm/observability_overview/has_data'); diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts index 96ac3c3a5e494..6153ddd46a5b4 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -11,9 +12,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:00:00.000Z'); - const end = encodeURIComponent('2020-06-29T10:00:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const bucketSize = '60s'; describe('Observability overview', () => { @@ -23,37 +27,58 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` ); expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceCount": 0, - "transactionCoordinates": Array [], - } - `); + + expect(response.body.serviceCount).to.be(0); + expect(response.body.transactionCoordinates.length).to.be(0); }); }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the service count and transaction coordinates', async () => { const response = await supertest.get( `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` ); expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceCount": 3, - "transactionCoordinates": Array [ - Object { - "x": 1593413220000, - "y": 0.016666666666666666, - }, - Object { - "x": 1593413280000, - "y": 1.0458333333333334, - }, - ], - } + + expect(response.body.serviceCount).to.be.greaterThan(0); + expect(response.body.transactionCoordinates.length).to.be.greaterThan(0); + + expectSnapshot(response.body.serviceCount).toMatchInline(`7`); + + expectSnapshot(response.body.transactionCoordinates.length).toMatchInline(`60`); + + expectSnapshot( + response.body.transactionCoordinates + .slice(0, 5) + .map(({ x, y }: { x: number; y: number }) => ({ + x: new Date(x).toISOString(), + y, + })) + ).toMatchInline(` + Array [ + Object { + "x": "2020-09-10T06:00:00.000Z", + "y": 1.2166666666666666, + }, + Object { + "x": "2020-09-10T06:01:00.000Z", + "y": 0.5, + }, + Object { + "x": "2020-09-10T06:02:00.000Z", + "y": 1.0333333333333334, + }, + Object { + "x": "2020-09-10T06:03:00.000Z", + "y": 0.55, + }, + Object { + "x": "2020-09-10T06:04:00.000Z", + "y": 1.15, + }, + ] `); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts index b0e503eb7d1eb..d729680154c1d 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts @@ -5,22 +5,28 @@ */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import archives_metadata from '../../../common/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); describe('Service Maps', () => { it('is only be available to users with Platinum license (or higher)', async () => { const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); expect(response.status).to.be(403); - expect(response.body.message).to.be( - "In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data." + + expectSnapshot(response.body.message).toMatchInline( + `"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."` ); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts index e4cceca573ce8..ee835fde680e0 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts @@ -12,7 +12,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const range = archives['apm_8.0.0']; + const archiveName = 'apm_8.0.0'; + const range = archives[archiveName]; const start = encodeURIComponent(range.start); const end = encodeURIComponent(range.end); @@ -29,8 +30,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('apm_8.0.0')); - after(() => esArchiver.unload('apm_8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the agent name', async () => { const response = await supertest.get( @@ -38,6 +39,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); + expect(response.body).to.eql({ agentName: 'nodejs' }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 8d91f4542e454..c5027565fd6b9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -4,18 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; import expect from '@kbn/expect'; +import { isEmpty, pick } from 'lodash'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + const uiFilters = encodeURIComponent(JSON.stringify({})); describe('APM Services Overview', () => { @@ -26,57 +33,196 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ hasHistoricalData: false, hasLegacyData: false, items: [] }); + expect(response.body.hasHistoricalData).to.be(false); + expect(response.body.hasLegacyData).to.be(false); + expect(response.body.items.length).to.be(0); }); }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('returns a list of services', async () => { - const response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - // sort services to mitigate unstable sort order - const services = sortBy(response.body.items, ['serviceName']); + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); - expect(response.status).to.be(200); - expectSnapshot(services).toMatchInline(` - Array [ - Object { - "agentName": "rum-js", - "avgResponseTime": 116375, - "environments": Array [], - "errorsPerMinute": 2.75, - "serviceName": "client", - "transactionsPerMinute": 2, - }, - Object { - "agentName": "java", - "avgResponseTime": 25636.349593495936, - "environments": Array [ + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('returns hasHistoricalData: true', () => { + expect(response.body.hasHistoricalData).to.be(true); + }); + + it('returns hasLegacyData: false', () => { + expect(response.body.hasLegacyData).to.be(false); + }); + + it('returns the correct service names', () => { + expectSnapshot(response.body.items.map((item: any) => item.serviceName)).toMatchInline(` + Array [ + "opbeans-python", + "opbeans-node", + "opbeans-ruby", + "opbeans-go", + "opbeans-dotnet", + "opbeans-java", + "opbeans-rum", + ] + `); + }); + + it('returns the correct metrics averages', () => { + expectSnapshot( + response.body.items.map((item: any) => + pick( + item, + 'transactionErrorRate.value', + 'avgResponseTime.value', + 'transactionsPerMinute.value' + ) + ) + ).toMatchInline(` + Array [ + Object { + "avgResponseTime": Object { + "value": 208079.9121184089, + }, + "transactionErrorRate": Object { + "value": 0.041666666666666664, + }, + "transactionsPerMinute": Object { + "value": 18.016666666666666, + }, + }, + Object { + "avgResponseTime": Object { + "value": 578297.1431623931, + }, + "transactionErrorRate": Object { + "value": 0.03317535545023697, + }, + "transactionsPerMinute": Object { + "value": 7.8, + }, + }, + Object { + "avgResponseTime": Object { + "value": 60518.587926509186, + }, + "transactionErrorRate": Object { + "value": 0.013123359580052493, + }, + "transactionsPerMinute": Object { + "value": 6.35, + }, + }, + Object { + "avgResponseTime": Object { + "value": 25259.78717201166, + }, + "transactionErrorRate": Object { + "value": 0.014577259475218658, + }, + "transactionsPerMinute": Object { + "value": 5.716666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 527290.3218390804, + }, + "transactionErrorRate": Object { + "value": 0.01532567049808429, + }, + "transactionsPerMinute": Object { + "value": 4.35, + }, + }, + Object { + "avgResponseTime": Object { + "value": 530245.8571428572, + }, + "transactionErrorRate": Object { + "value": 0.15384615384615385, + }, + "transactionsPerMinute": Object { + "value": 3.033333333333333, + }, + }, + Object { + "avgResponseTime": Object { + "value": 896134.328358209, + }, + "transactionsPerMinute": Object { + "value": 2.2333333333333334, + }, + }, + ] + `); + }); + + it('returns environments', () => { + expectSnapshot(response.body.items.map((item: any) => item.environments ?? [])) + .toMatchInline(` + Array [ + Array [ "production", ], - "errorsPerMinute": 4.5, - "serviceName": "opbeans-java", - "transactionsPerMinute": 30.75, - }, - Object { - "agentName": "nodejs", - "avgResponseTime": 38682.52419354839, - "environments": Array [ + Array [ + "testing", + ], + Array [ "production", ], - "errorsPerMinute": 3.75, - "serviceName": "opbeans-node", - "transactionsPerMinute": 31, - }, - ] - `); - - expect(response.body.hasHistoricalData).to.be(true); - expect(response.body.hasLegacyData).to.be(false); + Array [ + "testing", + ], + Array [ + "production", + ], + Array [ + "production", + ], + Array [ + "testing", + ], + ] + `); + }); + + it(`RUM services don't report any transaction error rates`, () => { + // RUM transactions don't have event.outcome set, + // so they should not have an error rate + + const rumServices = response.body.items.filter( + (item: any) => item.agentName === 'rum-js' + ); + + expect(rumServices.length).to.be.greaterThan(0); + + expect(rumServices.every((item: any) => isEmpty(item.transactionErrorRate?.value))); + }); + + it('non-RUM services all report transaction error rates', () => { + const nonRumServices = response.body.items.filter( + (item: any) => item.agentName !== 'rum-js' + ); + + expect( + nonRumServices.every((item: any) => { + return ( + typeof item.transactionErrorRate?.value === 'number' && + item.transactionErrorRate.timeseries.length > 0 + ); + }) + ).to.be(true); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts index a6c6bad21a8b7..1221ce0198d82 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -12,9 +13,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); describe('Transaction types', () => { describe('when data is not loaded ', () => { @@ -30,8 +34,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('handles empty state', async () => { const response = await supertest.get( @@ -39,11 +43,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); + expect(response.body.transactionTypes.length).to.be.greaterThan(0); + expectSnapshot(response.body).toMatchInline(` Object { "transactionTypes": Array [ - "request", "Worker", + "request", ], } `); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts index 27023d16f57ca..70ddf276ab35c 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { AgentConfigurationIntake } from '../../../../../plugins/apm/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '../../../../../plugins/apm/server/routes/settings/agent_configuration'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -16,6 +17,8 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte const log = getService('log'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + function getServices() { return supertestRead .get(`/api/apm/settings/agent-configuration/services`) @@ -125,31 +128,46 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns all services', async () => { const { body } = await getServices(); - expect(body).to.eql([ - 'ALL_OPTION_VALUE', - 'client', - 'opbeans-dotnet', - 'opbeans-go', - 'opbeans-java', - 'opbeans-node', - 'opbeans-python', - 'opbeans-ruby', - 'opbeans-rum', - ]); + expectSnapshot(body).toMatchInline(` + Array [ + "ALL_OPTION_VALUE", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ] + `); }); - it('returns the environments', async () => { + it('returns the environments, all unconfigured', async () => { const { body } = await getEnvironments('opbeans-node'); - expect(body).to.eql([ - { name: 'ALL_OPTION_VALUE', alreadyConfigured: false }, - { name: 'testing', alreadyConfigured: false }, - { name: 'production', alreadyConfigured: false }, - ]); + + expect(body.map((item: { name: string }) => item.name)).to.contain('ALL_OPTION_VALUE'); + + expect( + body.every((item: { alreadyConfigured: boolean }) => item.alreadyConfigured === false) + ).to.be(true); + + expectSnapshot(body).toMatchInline(` + Array [ + Object { + "alreadyConfigured": false, + "name": "ALL_OPTION_VALUE", + }, + Object { + "alreadyConfigured": false, + "name": "testing", + }, + ] + `); }); it('returns the agent names', async () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index 2acc6522bf479..a1c647a854bf6 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -14,6 +14,8 @@ export default function customLinksTests({ getService }: FtrProviderContext) { const log = getService('log'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + function searchCustomLinks(filters?: any) { const path = URL.format({ pathname: `/api/apm/settings/custom_links`, @@ -142,8 +144,8 @@ export default function customLinksTests({ getService }: FtrProviderContext) { }); describe('transaction', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('fetches a transaction sample', async () => { const response = await supertestRead.get( diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap index 5557e0828a338..2d382fa5fa4d3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap @@ -3,301 +3,598 @@ exports[`Top traces when data is loaded returns the correct buckets 1`] = ` Array [ Object { - "averageResponseTime": 2577, + "averageResponseTime": 3853, "impact": 0, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /throw-error", + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#create", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.016666666666666666, }, Object { - "averageResponseTime": 3147, - "impact": 0.06552270160444405, + "averageResponseTime": 5420, + "impact": 0.0013411780236742999, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#orders", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.016666666666666666, }, Object { - "averageResponseTime": 3392.5, - "impact": 0.09374344413758617, + "averageResponseTime": 4135.5, + "impact": 0.0037813174911251156, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order", + "service.name": "opbeans-node", + "transaction.name": "GET /api/types", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 4713.5, - "impact": 0.24559517890858723, + "averageResponseTime": 11058, + "impact": 0.006166680064182087, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#product", + "transaction.name": "APIRestController#products", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.016666666666666666, + }, + Object { + "averageResponseTime": 6014, + "impact": 0.0069968923698388, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "POST /api/orders", + }, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 4757, - "impact": 0.25059559560997896, + "averageResponseTime": 13540, + "impact": 0.00829099649989339, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product", + }, + "transactionsPerMinute": 0.016666666666666666, + }, + Object { + "averageResponseTime": 8710, + "impact": 0.011611845722520248, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers", + "transaction.name": "GET /api/types/:id", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 6787, - "impact": 0.4839483750082622, + "averageResponseTime": 10157, + "impact": 0.014088788415891928, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#products", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product_type", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 4749.666666666667, - "impact": 0.5227447114845778, + "averageResponseTime": 6944.333333333333, + "impact": 0.014532994793867014, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/orders/:id", }, - "transactionsPerMinute": 0.75, + "transactionsPerMinute": 0.05, }, Object { - "averageResponseTime": 7624.5, - "impact": 0.5802207655235637, + "averageResponseTime": 8438.333333333334, + "impact": 0.018369089179385976, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders", + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customer", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.05, }, Object { - "averageResponseTime": 5098, - "impact": 0.582807187955318, + "averageResponseTime": 13202, + "impact": 0.019301152273056246, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/stats", + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customers", }, - "transactionsPerMinute": 0.75, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 8181, - "impact": 0.6441916136689552, + "averageResponseTime": 9311, + "impact": 0.020609806515684198, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/types/:id", + "transaction.name": "GET /api/products/:id", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.05, }, Object { - "averageResponseTime": 20011, - "impact": 0.853921734857215, + "averageResponseTime": 14019, + "impact": 0.020699674858049102, "key": Object { "service.name": "opbeans-node", - "transaction.name": "POST /api", + "transaction.name": "GET /api/customers/:id", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 0.03333333333333333, + }, + Object { + "averageResponseTime": 28176, + "impact": 0.020817787536585832, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "POST opbeans.views.post_order", + }, + "transactionsPerMinute": 0.016666666666666666, }, Object { - "averageResponseTime": 6583, - "impact": 1.2172278724376455, + "averageResponseTime": 9298.75, + "impact": 0.02853705020124346, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products", }, - "transactionsPerMinute": 1, + "transactionsPerMinute": 0.06666666666666667, }, Object { - "averageResponseTime": 33097, - "impact": 1.6060533780113861, + "averageResponseTime": 7441.6, + "impact": 0.028548176757917213, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/top", + "service.name": "opbeans-java", + "transaction.name": "APIRestController#order", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 0.08333333333333333, }, Object { - "averageResponseTime": 4825, - "impact": 1.6450221426498186, + "averageResponseTime": 6260.166666666667, + "impact": 0.028850305566058266, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#topProducts", }, - "transactionsPerMinute": 1.75, + "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 35846, - "impact": 1.7640550505645587, + "averageResponseTime": 7656.2, + "impact": 0.029466545627989022, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /log-error", + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customerWhoBought", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 0.08333333333333333, + }, + Object { + "averageResponseTime": 7016.5, + "impact": 0.032734329734171834, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#show", + }, + "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 3742.153846153846, - "impact": 2.4998634943716573, + "averageResponseTime": 21102.5, + "impact": 0.03282505396551165, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.customer", + }, + "transactionsPerMinute": 0.03333333333333333, + }, + Object { + "averageResponseTime": 14443.333333333334, + "impact": 0.033787929062278454, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#customerWhoBought", + "transaction.name": "APIRestController#stats", }, - "transactionsPerMinute": 3.25, + "transactionsPerMinute": 0.05, }, Object { - "averageResponseTime": 3492.9285714285716, - "impact": 2.5144049360435208, + "averageResponseTime": 2828.0625, + "impact": 0.0354303800051189, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET static file", + "service.name": "opbeans-java", + "transaction.name": "ResourceHttpRequestHandler", + }, + "transactionsPerMinute": 0.26666666666666666, + }, + Object { + "averageResponseTime": 9920.8, + "impact": 0.03915777649082508, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#product", }, - "transactionsPerMinute": 3.5, + "transactionsPerMinute": 0.08333333333333333, }, Object { - "averageResponseTime": 26992.5, - "impact": 2.8066131947777255, + "averageResponseTime": 16860.333333333332, + "impact": 0.03999398001930612, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.orders", + }, + "transactionsPerMinute": 0.05, + }, + Object { + "averageResponseTime": 10264.8, + "impact": 0.04062990552765966, "key": Object { "service.name": "opbeans-node", + "transaction.name": "GET /api/products/top", + }, + "transactionsPerMinute": 0.08333333333333333, + }, + Object { + "averageResponseTime": 8818.833333333334, + "impact": 0.04198991310878184, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.order", + }, + "transactionsPerMinute": 0.1, + }, + Object { + "averageResponseTime": 4649.307692307692, + "impact": 0.04843304531185787, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/types/:id", + }, + "transactionsPerMinute": 0.21666666666666667, + }, + Object { + "averageResponseTime": 30425, + "impact": 0.048783103902593536, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.products", + }, + "transactionsPerMinute": 0.03333333333333333, + }, + Object { + "averageResponseTime": 4215.2, + "impact": 0.05081840788491484, + "key": Object { + "service.name": "opbeans-go", "transaction.name": "GET /api/types", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.25, }, Object { - "averageResponseTime": 13516.5, - "impact": 2.8112687551548836, + "averageResponseTime": 7333.777777777777, + "impact": 0.053194355679247865, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id", + "service.name": "opbeans-ruby", + "transaction.name": "Api::StatsController#index", + }, + "transactionsPerMinute": 0.15, + }, + Object { + "averageResponseTime": 7562.111111111111, + "impact": 0.05495320752267524, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::TypesController#index", + }, + "transactionsPerMinute": 0.15, + }, + Object { + "averageResponseTime": 5459.307692307692, + "impact": 0.057445556217595194, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/products", + }, + "transactionsPerMinute": 0.21666666666666667, + }, + Object { + "averageResponseTime": 7248.5, + "impact": 0.058741372125599586, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::TypesController#show", + }, + "transactionsPerMinute": 0.16666666666666666, + }, + Object { + "averageResponseTime": 87771, + "impact": 0.07182449099597951, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.top_products", + }, + "transactionsPerMinute": 0.016666666666666666, + }, + Object { + "averageResponseTime": 6161.2, + "impact": 0.0758018070623576, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/orders/:id", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 19260.8, + "impact": 0.07912779161883388, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product_types", + }, + "transactionsPerMinute": 0.08333333333333333, + }, + Object { + "averageResponseTime": 10632.4, + "impact": 0.08770379914737025, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::ProductsController#show", + }, + "transactionsPerMinute": 0.16666666666666666, + }, + Object { + "averageResponseTime": 54309.5, + "impact": 0.08966806434477453, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.customers", }, - "transactionsPerMinute": 1, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 20092, - "impact": 3.168195050736987, + "averageResponseTime": 5046.695652173913, + "impact": 0.09604871665268258, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/customers/:id", + }, + "transactionsPerMinute": 0.38333333333333336, + }, + Object { + "averageResponseTime": 20346, + "impact": 0.10118576228005537, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/customers", + "transaction.name": "GET /api/stats", }, - "transactionsPerMinute": 0.75, + "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 15535, - "impact": 3.275330415465657, + "averageResponseTime": 18472.85714285714, + "impact": 0.10737726312450965, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#stats", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.stats", }, - "transactionsPerMinute": 1, + "transactionsPerMinute": 0.11666666666666667, }, Object { - "averageResponseTime": 32667.5, - "impact": 3.458966408120217, + "averageResponseTime": 32662, + "impact": 0.10852244257293098, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /log-message", + "transaction.name": "GET /api/customers", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.06666666666666667, }, Object { - "averageResponseTime": 16690.75, - "impact": 3.541042213287889, + "averageResponseTime": 13975.7, + "impact": 0.11631873524532996, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customers", + "service.name": "opbeans-ruby", + "transaction.name": "Api::ProductsController#index", }, - "transactionsPerMinute": 1, + "transactionsPerMinute": 0.16666666666666666, }, Object { - "averageResponseTime": 33500, - "impact": 3.5546640380951287, + "averageResponseTime": 13373.615384615385, + "impact": 0.14550454928955053, "key": Object { - "service.name": "client", - "transaction.name": "/customers", + "service.name": "opbeans-go", + "transaction.name": "GET /api/orders", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.21666666666666667, }, Object { - "averageResponseTime": 77000, - "impact": 4.129424578484989, + "averageResponseTime": 19675.333333333332, + "impact": 0.14826136767771575, "key": Object { - "service.name": "client", - "transaction.name": "/products", + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#index", + }, + "transactionsPerMinute": 0.15, + }, + Object { + "averageResponseTime": 12946.266666666666, + "impact": 0.1629107633721697, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::CustomersController#show", }, "transactionsPerMinute": 0.25, }, Object { - "averageResponseTime": 19370.6, - "impact": 5.270496679320978, + "averageResponseTime": 16506.666666666668, + "impact": 0.16623674792864598, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customer", + "service.name": "opbeans-go", + "transaction.name": "GET /api/products/:id/customers", }, - "transactionsPerMinute": 1.25, + "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 81500, - "impact": 9.072365225837785, + "averageResponseTime": 17101.5, + "impact": 0.1723460834315095, "key": Object { - "service.name": "client", - "transaction.name": "/orders", + "service.name": "opbeans-ruby", + "transaction.name": "Api::ProductsController#top", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 14419.42857142857, - "impact": 11.30657439844125, + "averageResponseTime": 15871.3125, + "impact": 0.21404756195574876, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "ResourceHttpRequestHandler", + "service.name": "opbeans-go", + "transaction.name": "GET /api/stats", }, - "transactionsPerMinute": 3.5, + "transactionsPerMinute": 0.26666666666666666, }, Object { - "averageResponseTime": 270684, - "impact": 15.261616628971955, + "averageResponseTime": 11237.785714285714, + "impact": 0.26601457284498453, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::CustomersController#index", + }, + "transactionsPerMinute": 0.4666666666666667, + }, + Object { + "averageResponseTime": 15403.40909090909, + "impact": 0.28674163615023057, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/products/:id", + }, + "transactionsPerMinute": 0.36666666666666664, + }, + Object { + "averageResponseTime": 101902.16666666667, + "impact": 0.5200039055925703, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product_customers", + }, + "transactionsPerMinute": 0.1, + }, + Object { + "averageResponseTime": 32236.133333333335, + "impact": 0.82441879318559, "key": Object { "service.name": "opbeans-node", - "transaction.name": "POST /api/orders", + "transaction.name": "GET /api", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 94012.11111111111, + "impact": 1.445052989113503, + "key": Object { + "service.name": "opbeans-go", + "transaction.name": "GET /api/customers", + }, + "transactionsPerMinute": 0.3, + }, + Object { + "averageResponseTime": 32260.39837398374, + "impact": 3.3928945329783606, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Rack", + }, + "transactionsPerMinute": 2.05, + }, + Object { + "averageResponseTime": 34207.61666666667, + "impact": 3.5100528953080716, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "opbeans.tasks.sync_orders", + }, + "transactionsPerMinute": 2, + }, + Object { + "averageResponseTime": 638040, + "impact": 13.648987298470669, + "key": Object { + "service.name": "opbeans-rum", + "transaction.name": "/customers", + }, + "transactionsPerMinute": 0.4166666666666667, }, Object { - "averageResponseTime": 36010.53846153846, - "impact": 26.61043592713186, + "averageResponseTime": 2061418.6666666667, + "impact": 15.875811844928256, "key": Object { "service.name": "opbeans-java", "transaction.name": "DispatcherServlet#doGet", }, - "transactionsPerMinute": 3.25, + "transactionsPerMinute": 0.15, }, Object { - "averageResponseTime": 208000, - "impact": 35.56882613781033, + "averageResponseTime": 847846.1538461539, + "impact": 18.8639188225597, "key": Object { - "service.name": "client", + "service.name": "opbeans-rum", + "transaction.name": "/orders", + }, + "transactionsPerMinute": 0.43333333333333335, + }, + Object { + "averageResponseTime": 1091031.25, + "impact": 29.87835404059707, + "key": Object { + "service.name": "opbeans-rum", + "transaction.name": "/products", + }, + "transactionsPerMinute": 0.5333333333333333, + }, + Object { + "averageResponseTime": 924980.3921568628, + "impact": 40.37240876189292, + "key": Object { + "service.name": "opbeans-rum", "transaction.name": "/dashboard", }, - "transactionsPerMinute": 0.75, + "transactionsPerMinute": 0.85, }, Object { - "averageResponseTime": 49816.15625, - "impact": 91.32732325394932, + "averageResponseTime": 979844.2117647058, + "impact": 71.28092018746297, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api", + "transaction.name": "Process completed order", }, - "transactionsPerMinute": 8, + "transactionsPerMinute": 1.4166666666666667, }, Object { - "averageResponseTime": 1745009, - "impact": 100, + "averageResponseTime": 996808.380952381, + "impact": 71.66191574108551, "key": Object { "service.name": "opbeans-node", "transaction.name": "Process payment", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 1.4, + }, + Object { + "averageResponseTime": 1083442.5568181819, + "impact": 81.59967772014184, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "Update shipping status", + }, + "transactionsPerMinute": 1.4666666666666666, + }, + Object { + "averageResponseTime": 134550.32361111112, + "impact": 82.91200201469418, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "opbeans.tasks.update_stats", + }, + "transactionsPerMinute": 12, + }, + Object { + "averageResponseTime": 1600567.6301369863, + "impact": 100, + "key": Object { + "service.name": "opbeans-python", + "transaction.name": "opbeans.tasks.sync_customers", + }, + "transactionsPerMinute": 1.2166666666666666, }, ] `; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index 2935fb8e2839a..da4bd0aa4f0d9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; import { sortBy, omit } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -12,9 +13,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({})); describe('Top traces', () => { @@ -25,32 +29,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "bucketSize": 1000, - "isAggregationAccurate": true, - "items": Array [], - } - `); + expect(response.body.items.length).to.be(0); + expect(response.body.isAggregationAccurate).to.be(true); }); }); describe('when data is loaded', () => { let response: any; before(async () => { - await esArchiver.load('8.0.0'); + await esArchiver.load(archiveName); response = await supertest.get( `/api/apm/traces?start=${start}&end=${end}&uiFilters=${uiFilters}` ); }); - after(() => esArchiver.unload('8.0.0')); + after(() => esArchiver.unload(archiveName)); it('returns the correct status code', async () => { expect(response.status).to.be(200); }); it('returns the correct number of buckets', async () => { - expectSnapshot(response.body.items.length).toMatchInline(`33`); + expectSnapshot(response.body.items.length).toMatchInline(`66`); }); it('returns the correct buckets', async () => { @@ -68,49 +67,49 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(firstItem).toMatchInline(` Object { - "averageResponseTime": 2577, + "averageResponseTime": 3853, "impact": 0, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /throw-error", + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#create", }, - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.016666666666666666, } `); expectSnapshot(lastItem).toMatchInline(` Object { - "averageResponseTime": 1745009, + "averageResponseTime": 1600567.6301369863, "impact": 100, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Process payment", + "service.name": "opbeans-python", + "transaction.name": "opbeans.tasks.sync_customers", }, - "transactionsPerMinute": 0.25, + "transactionsPerMinute": 1.2166666666666666, } `); expectSnapshot(groups).toMatchInline(` Array [ Object { - "service.name": "opbeans-node", - "transaction.name": "GET /throw-error", + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#create", }, Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#orders", }, Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order", + "service.name": "opbeans-node", + "transaction.name": "GET /api/types", }, Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#product", + "transaction.name": "APIRestController#products", }, Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers", + "service.name": "opbeans-go", + "transaction.name": "POST /api/orders", }, ] `); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap index 326797919a095..37473ee008b3d 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap @@ -5,1469 +5,850 @@ Array [ Object { "data": Array [ Object { - "x": 1593413100000, + "x": 1599717600000, + "y": 999000, }, Object { - "x": 1593413101000, + "x": 1599717630000, + "y": 1111000, }, Object { - "x": 1593413102000, + "x": 1599717660000, + "y": 600000, }, Object { - "x": 1593413103000, + "x": 1599717690000, }, Object { - "x": 1593413104000, + "x": 1599717720000, }, Object { - "x": 1593413105000, + "x": 1599717750000, + "y": 1487000, }, Object { - "x": 1593413106000, + "x": 1599717780000, + "y": 312000, }, Object { - "x": 1593413107000, + "x": 1599717810000, }, Object { - "x": 1593413108000, + "x": 1599717840000, }, Object { - "x": 1593413109000, + "x": 1599717870000, + "y": 2073000, }, Object { - "x": 1593413110000, + "x": 1599717900000, + "y": 791333.3333333334, }, Object { - "x": 1593413111000, + "x": 1599717930000, + "y": 604000, }, Object { - "x": 1593413112000, + "x": 1599717960000, }, Object { - "x": 1593413113000, + "x": 1599717990000, + "y": 792000, }, Object { - "x": 1593413114000, + "x": 1599718020000, + "y": 596000, }, Object { - "x": 1593413115000, + "x": 1599718050000, + "y": 661000, }, Object { - "x": 1593413116000, + "x": 1599718080000, }, Object { - "x": 1593413117000, + "x": 1599718110000, + "y": 1016000, }, Object { - "x": 1593413118000, + "x": 1599718140000, + "y": 732000, }, Object { - "x": 1593413119000, + "x": 1599718170000, }, Object { - "x": 1593413120000, + "x": 1599718200000, }, Object { - "x": 1593413121000, + "x": 1599718230000, + "y": 1578000, }, Object { - "x": 1593413122000, + "x": 1599718260000, + "y": 450000, }, Object { - "x": 1593413123000, + "x": 1599718290000, + "y": 911000, }, Object { - "x": 1593413124000, + "x": 1599718320000, }, Object { - "x": 1593413125000, + "x": 1599718350000, + "y": 1599000, }, Object { - "x": 1593413126000, + "x": 1599718380000, + "y": 661000, }, Object { - "x": 1593413127000, + "x": 1599718410000, + "y": 596000, }, Object { - "x": 1593413128000, + "x": 1599718440000, }, Object { - "x": 1593413129000, + "x": 1599718470000, + "y": 823000, }, Object { - "x": 1593413130000, + "x": 1599718500000, + "y": 551666.6666666666, }, Object { - "x": 1593413131000, + "x": 1599718530000, }, Object { - "x": 1593413132000, + "x": 1599718560000, }, Object { - "x": 1593413133000, + "x": 1599718590000, + "y": 1311000, }, Object { - "x": 1593413134000, + "x": 1599718620000, + "y": 574000, }, Object { - "x": 1593413135000, + "x": 1599718650000, }, Object { - "x": 1593413136000, + "x": 1599718680000, }, Object { - "x": 1593413137000, + "x": 1599718710000, + "y": 1806000, }, Object { - "x": 1593413138000, + "x": 1599718740000, + "y": 563000, }, Object { - "x": 1593413139000, + "x": 1599718770000, + "y": 665000, }, Object { - "x": 1593413140000, + "x": 1599718800000, }, Object { - "x": 1593413141000, + "x": 1599718830000, + "y": 1202000, }, Object { - "x": 1593413142000, + "x": 1599718860000, + "y": 542666.6666666666, }, Object { - "x": 1593413143000, + "x": 1599718890000, + "y": 688000, }, Object { - "x": 1593413144000, + "x": 1599718920000, }, Object { - "x": 1593413145000, + "x": 1599718950000, + "y": 995000, }, Object { - "x": 1593413146000, + "x": 1599718980000, + "y": 647000, }, Object { - "x": 1593413147000, + "x": 1599719010000, + "y": 552000, }, Object { - "x": 1593413148000, + "x": 1599719040000, }, Object { - "x": 1593413149000, + "x": 1599719070000, + "y": 1760500, }, Object { - "x": 1593413150000, + "x": 1599719100000, + "y": 556000, }, Object { - "x": 1593413151000, + "x": 1599719130000, + "y": 603000, }, Object { - "x": 1593413152000, + "x": 1599719160000, }, Object { - "x": 1593413153000, + "x": 1599719190000, + "y": 920000, }, Object { - "x": 1593413154000, + "x": 1599719220000, + "y": 523000, }, Object { - "x": 1593413155000, + "x": 1599719250000, }, Object { - "x": 1593413156000, + "x": 1599719280000, }, Object { - "x": 1593413157000, + "x": 1599719310000, + "y": 1074000, }, Object { - "x": 1593413158000, + "x": 1599719340000, + "y": 723500, }, Object { - "x": 1593413159000, + "x": 1599719370000, }, Object { - "x": 1593413160000, + "x": 1599719400000, }, Object { - "x": 1593413161000, + "x": 1599719430000, + "y": 1436000, }, Object { - "x": 1593413162000, + "x": 1599719460000, + "y": 614000, }, Object { - "x": 1593413163000, + "x": 1599719490000, + "y": 789000, }, Object { - "x": 1593413164000, + "x": 1599719520000, }, Object { - "x": 1593413165000, + "x": 1599719550000, + "y": 1919000, }, Object { - "x": 1593413166000, + "x": 1599719580000, + "y": 881000, }, Object { - "x": 1593413167000, + "x": 1599719610000, }, Object { - "x": 1593413168000, + "x": 1599719640000, }, Object { - "x": 1593413169000, + "x": 1599719670000, + "y": 1083000, }, Object { - "x": 1593413170000, + "x": 1599719700000, + "y": 603000, }, Object { - "x": 1593413171000, + "x": 1599719730000, + "y": 906000, }, Object { - "x": 1593413172000, + "x": 1599719760000, }, Object { - "x": 1593413173000, + "x": 1599719790000, + "y": 1222000, }, Object { - "x": 1593413174000, + "x": 1599719820000, + "y": 608000, }, Object { - "x": 1593413175000, + "x": 1599719850000, }, Object { - "x": 1593413176000, + "x": 1599719880000, }, Object { - "x": 1593413177000, + "x": 1599719910000, + "y": 805000, }, Object { - "x": 1593413178000, + "x": 1599719940000, + "y": 477000, }, Object { - "x": 1593413179000, + "x": 1599719970000, + "y": 652000, }, Object { - "x": 1593413180000, + "x": 1599720000000, }, Object { - "x": 1593413181000, + "x": 1599720030000, + "y": 1417000, }, Object { - "x": 1593413182000, + "x": 1599720060000, + "y": 545250, }, Object { - "x": 1593413183000, + "x": 1599720090000, }, Object { - "x": 1593413184000, + "x": 1599720120000, }, Object { - "x": 1593413185000, + "x": 1599720150000, + "y": 1122000, }, Object { - "x": 1593413186000, + "x": 1599720180000, + "y": 686000, }, Object { - "x": 1593413187000, + "x": 1599720210000, + "y": 1978000, }, Object { - "x": 1593413188000, + "x": 1599720240000, + "y": 927000, }, Object { - "x": 1593413189000, + "x": 1599720270000, + "y": 2112500, }, Object { - "x": 1593413190000, + "x": 1599720300000, + "y": 776333.3333333334, }, Object { - "x": 1593413191000, + "x": 1599720330000, }, Object { - "x": 1593413192000, + "x": 1599720360000, }, Object { - "x": 1593413193000, + "x": 1599720390000, + "y": 914000, }, Object { - "x": 1593413194000, + "x": 1599720420000, + "y": 534500, }, Object { - "x": 1593413195000, + "x": 1599720450000, }, Object { - "x": 1593413196000, + "x": 1599720480000, }, Object { - "x": 1593413197000, + "x": 1599720510000, + "y": 930000, }, Object { - "x": 1593413198000, + "x": 1599720540000, + "y": 501000, }, Object { - "x": 1593413199000, + "x": 1599720570000, + "y": 801500, }, Object { - "x": 1593413200000, + "x": 1599720600000, }, Object { - "x": 1593413201000, + "x": 1599720630000, + "y": 698000, }, Object { - "x": 1593413202000, + "x": 1599720660000, + "y": 626000, }, Object { - "x": 1593413203000, + "x": 1599720690000, }, Object { - "x": 1593413204000, + "x": 1599720720000, }, Object { - "x": 1593413205000, + "x": 1599720750000, + "y": 1091000, }, Object { - "x": 1593413206000, + "x": 1599720780000, + "y": 7822000, }, Object { - "x": 1593413207000, + "x": 1599720810000, }, Object { - "x": 1593413208000, + "x": 1599720840000, }, Object { - "x": 1593413209000, + "x": 1599720870000, + "y": 892000, }, Object { - "x": 1593413210000, + "x": 1599720900000, + "y": 591250, }, Object { - "x": 1593413211000, + "x": 1599720930000, }, Object { - "x": 1593413212000, + "x": 1599720960000, }, Object { - "x": 1593413213000, + "x": 1599720990000, + "y": 1096000, }, Object { - "x": 1593413214000, + "x": 1599721020000, + "y": 1087000, }, Object { - "x": 1593413215000, + "x": 1599721050000, }, Object { - "x": 1593413216000, + "x": 1599721080000, }, Object { - "x": 1593413217000, + "x": 1599721110000, + "y": 983000, }, Object { - "x": 1593413218000, + "x": 1599721140000, + "y": 801000, }, Object { - "x": 1593413219000, + "x": 1599721170000, }, Object { - "x": 1593413220000, - }, - Object { - "x": 1593413221000, - }, - Object { - "x": 1593413222000, - }, - Object { - "x": 1593413223000, - }, - Object { - "x": 1593413224000, - }, - Object { - "x": 1593413225000, - }, - Object { - "x": 1593413226000, - }, - Object { - "x": 1593413227000, - }, - Object { - "x": 1593413228000, - }, - Object { - "x": 1593413229000, - }, - Object { - "x": 1593413230000, - }, - Object { - "x": 1593413231000, - }, - Object { - "x": 1593413232000, - }, - Object { - "x": 1593413233000, - }, - Object { - "x": 1593413234000, - }, - Object { - "x": 1593413235000, - }, - Object { - "x": 1593413236000, - }, - Object { - "x": 1593413237000, - }, - Object { - "x": 1593413238000, - }, - Object { - "x": 1593413239000, - }, - Object { - "x": 1593413240000, - }, - Object { - "x": 1593413241000, - }, - Object { - "x": 1593413242000, - }, - Object { - "x": 1593413243000, - }, - Object { - "x": 1593413244000, - }, - Object { - "x": 1593413245000, - }, - Object { - "x": 1593413246000, - }, - Object { - "x": 1593413247000, - }, - Object { - "x": 1593413248000, - }, - Object { - "x": 1593413249000, - }, - Object { - "x": 1593413250000, - }, - Object { - "x": 1593413251000, - }, - Object { - "x": 1593413252000, - }, - Object { - "x": 1593413253000, - }, - Object { - "x": 1593413254000, - }, - Object { - "x": 1593413255000, - }, - Object { - "x": 1593413256000, - }, - Object { - "x": 1593413257000, - }, - Object { - "x": 1593413258000, - }, - Object { - "x": 1593413259000, - }, - Object { - "x": 1593413260000, - }, - Object { - "x": 1593413261000, - }, - Object { - "x": 1593413262000, - }, - Object { - "x": 1593413263000, - }, - Object { - "x": 1593413264000, - }, - Object { - "x": 1593413265000, - }, - Object { - "x": 1593413266000, - }, - Object { - "x": 1593413267000, - }, - Object { - "x": 1593413268000, - }, - Object { - "x": 1593413269000, - }, - Object { - "x": 1593413270000, - }, - Object { - "x": 1593413271000, - }, - Object { - "x": 1593413272000, - }, - Object { - "x": 1593413273000, - }, - Object { - "x": 1593413274000, - }, - Object { - "x": 1593413275000, - }, - Object { - "x": 1593413276000, - }, - Object { - "x": 1593413277000, - }, - Object { - "x": 1593413278000, - }, - Object { - "x": 1593413279000, - }, - Object { - "x": 1593413280000, - }, - Object { - "x": 1593413281000, - }, - Object { - "x": 1593413282000, - }, - Object { - "x": 1593413283000, - }, - Object { - "x": 1593413284000, - }, - Object { - "x": 1593413285000, - }, - Object { - "x": 1593413286000, - }, - Object { - "x": 1593413287000, - "y": 342000, - }, - Object { - "x": 1593413288000, - }, - Object { - "x": 1593413289000, - }, - Object { - "x": 1593413290000, - }, - Object { - "x": 1593413291000, - }, - Object { - "x": 1593413292000, - }, - Object { - "x": 1593413293000, - }, - Object { - "x": 1593413294000, - }, - Object { - "x": 1593413295000, - }, - Object { - "x": 1593413296000, - }, - Object { - "x": 1593413297000, - }, - Object { - "x": 1593413298000, - "y": 173000, - }, - Object { - "x": 1593413299000, - }, - Object { - "x": 1593413300000, - }, - Object { - "x": 1593413301000, - "y": 109000, - }, - Object { - "x": 1593413302000, - }, - Object { - "x": 1593413303000, - }, - Object { - "x": 1593413304000, - }, - Object { - "x": 1593413305000, - }, - Object { - "x": 1593413306000, - }, - Object { - "x": 1593413307000, - }, - Object { - "x": 1593413308000, - }, - Object { - "x": 1593413309000, - }, - Object { - "x": 1593413310000, - }, - Object { - "x": 1593413311000, - }, - Object { - "x": 1593413312000, - }, - Object { - "x": 1593413313000, - }, - Object { - "x": 1593413314000, - }, - Object { - "x": 1593413315000, - }, - Object { - "x": 1593413316000, - }, - Object { - "x": 1593413317000, - }, - Object { - "x": 1593413318000, - "y": 140000, - }, - Object { - "x": 1593413319000, - }, - Object { - "x": 1593413320000, - }, - Object { - "x": 1593413321000, - }, - Object { - "x": 1593413322000, - }, - Object { - "x": 1593413323000, - }, - Object { - "x": 1593413324000, - }, - Object { - "x": 1593413325000, - }, - Object { - "x": 1593413326000, - }, - Object { - "x": 1593413327000, - }, - Object { - "x": 1593413328000, - "y": 77000, - }, - Object { - "x": 1593413329000, - }, - Object { - "x": 1593413330000, - }, - Object { - "x": 1593413331000, - }, - Object { - "x": 1593413332000, - }, - Object { - "x": 1593413333000, - }, - Object { - "x": 1593413334000, - }, - Object { - "x": 1593413335000, - }, - Object { - "x": 1593413336000, - }, - Object { - "x": 1593413337000, - }, - Object { - "x": 1593413338000, - }, - Object { - "x": 1593413339000, - }, - Object { - "x": 1593413340000, + "x": 1599721200000, }, ], - "title": "HeadlessChrome", + "title": "Electron", }, ] `; -exports[`Average duration by browser when data is loaded returns the average duration by browser filtering by transaction name 1`] = ` +exports[`Average duration by browser when data is loaded returns the average duration by browser filtering by transaction name 2`] = ` Array [ Object { "data": Array [ Object { - "x": 1593413100000, - }, - Object { - "x": 1593413101000, - }, - Object { - "x": 1593413102000, - }, - Object { - "x": 1593413103000, - }, - Object { - "x": 1593413104000, - }, - Object { - "x": 1593413105000, - }, - Object { - "x": 1593413106000, - }, - Object { - "x": 1593413107000, - }, - Object { - "x": 1593413108000, - }, - Object { - "x": 1593413109000, - }, - Object { - "x": 1593413110000, - }, - Object { - "x": 1593413111000, - }, - Object { - "x": 1593413112000, - }, - Object { - "x": 1593413113000, - }, - Object { - "x": 1593413114000, - }, - Object { - "x": 1593413115000, - }, - Object { - "x": 1593413116000, - }, - Object { - "x": 1593413117000, - }, - Object { - "x": 1593413118000, - }, - Object { - "x": 1593413119000, - }, - Object { - "x": 1593413120000, - }, - Object { - "x": 1593413121000, - }, - Object { - "x": 1593413122000, - }, - Object { - "x": 1593413123000, - }, - Object { - "x": 1593413124000, - }, - Object { - "x": 1593413125000, - }, - Object { - "x": 1593413126000, - }, - Object { - "x": 1593413127000, - }, - Object { - "x": 1593413128000, - }, - Object { - "x": 1593413129000, - }, - Object { - "x": 1593413130000, - }, - Object { - "x": 1593413131000, - }, - Object { - "x": 1593413132000, - }, - Object { - "x": 1593413133000, - }, - Object { - "x": 1593413134000, - }, - Object { - "x": 1593413135000, - }, - Object { - "x": 1593413136000, - }, - Object { - "x": 1593413137000, - }, - Object { - "x": 1593413138000, - }, - Object { - "x": 1593413139000, - }, - Object { - "x": 1593413140000, - }, - Object { - "x": 1593413141000, - }, - Object { - "x": 1593413142000, - }, - Object { - "x": 1593413143000, - }, - Object { - "x": 1593413144000, - }, - Object { - "x": 1593413145000, - }, - Object { - "x": 1593413146000, - }, - Object { - "x": 1593413147000, - }, - Object { - "x": 1593413148000, - }, - Object { - "x": 1593413149000, - }, - Object { - "x": 1593413150000, - }, - Object { - "x": 1593413151000, - }, - Object { - "x": 1593413152000, - }, - Object { - "x": 1593413153000, - }, - Object { - "x": 1593413154000, - }, - Object { - "x": 1593413155000, - }, - Object { - "x": 1593413156000, - }, - Object { - "x": 1593413157000, - }, - Object { - "x": 1593413158000, - }, - Object { - "x": 1593413159000, - }, - Object { - "x": 1593413160000, - }, - Object { - "x": 1593413161000, - }, - Object { - "x": 1593413162000, - }, - Object { - "x": 1593413163000, - }, - Object { - "x": 1593413164000, - }, - Object { - "x": 1593413165000, - }, - Object { - "x": 1593413166000, - }, - Object { - "x": 1593413167000, - }, - Object { - "x": 1593413168000, - }, - Object { - "x": 1593413169000, - }, - Object { - "x": 1593413170000, - }, - Object { - "x": 1593413171000, - }, - Object { - "x": 1593413172000, - }, - Object { - "x": 1593413173000, - }, - Object { - "x": 1593413174000, - }, - Object { - "x": 1593413175000, - }, - Object { - "x": 1593413176000, - }, - Object { - "x": 1593413177000, - }, - Object { - "x": 1593413178000, - }, - Object { - "x": 1593413179000, - }, - Object { - "x": 1593413180000, - }, - Object { - "x": 1593413181000, - }, - Object { - "x": 1593413182000, - }, - Object { - "x": 1593413183000, - }, - Object { - "x": 1593413184000, - }, - Object { - "x": 1593413185000, - }, - Object { - "x": 1593413186000, - }, - Object { - "x": 1593413187000, - }, - Object { - "x": 1593413188000, - }, - Object { - "x": 1593413189000, - }, - Object { - "x": 1593413190000, - }, - Object { - "x": 1593413191000, - }, - Object { - "x": 1593413192000, - }, - Object { - "x": 1593413193000, - }, - Object { - "x": 1593413194000, - }, - Object { - "x": 1593413195000, - }, - Object { - "x": 1593413196000, - }, - Object { - "x": 1593413197000, - }, - Object { - "x": 1593413198000, - }, - Object { - "x": 1593413199000, - }, - Object { - "x": 1593413200000, - }, - Object { - "x": 1593413201000, - }, - Object { - "x": 1593413202000, - }, - Object { - "x": 1593413203000, - }, - Object { - "x": 1593413204000, - }, - Object { - "x": 1593413205000, - }, - Object { - "x": 1593413206000, - }, - Object { - "x": 1593413207000, - }, - Object { - "x": 1593413208000, - }, - Object { - "x": 1593413209000, - }, - Object { - "x": 1593413210000, - }, - Object { - "x": 1593413211000, - }, - Object { - "x": 1593413212000, - }, - Object { - "x": 1593413213000, - }, - Object { - "x": 1593413214000, - }, - Object { - "x": 1593413215000, - }, - Object { - "x": 1593413216000, - }, - Object { - "x": 1593413217000, - }, - Object { - "x": 1593413218000, - }, - Object { - "x": 1593413219000, - }, - Object { - "x": 1593413220000, + "x": 1599717600000, + "y": 999000, }, Object { - "x": 1593413221000, + "x": 1599717630000, + "y": 1111000, }, Object { - "x": 1593413222000, + "x": 1599717660000, }, Object { - "x": 1593413223000, + "x": 1599717690000, }, Object { - "x": 1593413224000, + "x": 1599717720000, }, Object { - "x": 1593413225000, + "x": 1599717750000, + "y": 1487000, }, Object { - "x": 1593413226000, + "x": 1599717780000, }, Object { - "x": 1593413227000, + "x": 1599717810000, }, Object { - "x": 1593413228000, + "x": 1599717840000, }, Object { - "x": 1593413229000, + "x": 1599717870000, + "y": 1326000, }, Object { - "x": 1593413230000, + "x": 1599717900000, }, Object { - "x": 1593413231000, + "x": 1599717930000, }, Object { - "x": 1593413232000, + "x": 1599717960000, }, Object { - "x": 1593413233000, + "x": 1599717990000, + "y": 792000, }, Object { - "x": 1593413234000, + "x": 1599718020000, }, Object { - "x": 1593413235000, + "x": 1599718050000, }, Object { - "x": 1593413236000, + "x": 1599718080000, }, Object { - "x": 1593413237000, + "x": 1599718110000, + "y": 1016000, }, Object { - "x": 1593413238000, + "x": 1599718140000, }, Object { - "x": 1593413239000, + "x": 1599718170000, }, Object { - "x": 1593413240000, + "x": 1599718200000, }, Object { - "x": 1593413241000, + "x": 1599718230000, + "y": 1578000, }, Object { - "x": 1593413242000, + "x": 1599718260000, }, Object { - "x": 1593413243000, + "x": 1599718290000, }, Object { - "x": 1593413244000, + "x": 1599718320000, }, Object { - "x": 1593413245000, + "x": 1599718350000, + "y": 1020000, }, Object { - "x": 1593413246000, + "x": 1599718380000, }, Object { - "x": 1593413247000, + "x": 1599718410000, }, Object { - "x": 1593413248000, + "x": 1599718440000, }, Object { - "x": 1593413249000, + "x": 1599718470000, + "y": 823000, }, Object { - "x": 1593413250000, + "x": 1599718500000, }, Object { - "x": 1593413251000, + "x": 1599718530000, }, Object { - "x": 1593413252000, + "x": 1599718560000, }, Object { - "x": 1593413253000, + "x": 1599718590000, + "y": 1311000, }, Object { - "x": 1593413254000, + "x": 1599718620000, }, Object { - "x": 1593413255000, + "x": 1599718650000, }, Object { - "x": 1593413256000, + "x": 1599718680000, }, Object { - "x": 1593413257000, + "x": 1599718710000, + "y": 1806000, }, Object { - "x": 1593413258000, + "x": 1599718740000, }, Object { - "x": 1593413259000, + "x": 1599718770000, }, Object { - "x": 1593413260000, + "x": 1599718800000, }, Object { - "x": 1593413261000, + "x": 1599718830000, + "y": 1202000, }, Object { - "x": 1593413262000, + "x": 1599718860000, }, Object { - "x": 1593413263000, + "x": 1599718890000, }, Object { - "x": 1593413264000, + "x": 1599718920000, }, Object { - "x": 1593413265000, + "x": 1599718950000, + "y": 995000, }, Object { - "x": 1593413266000, + "x": 1599718980000, }, Object { - "x": 1593413267000, + "x": 1599719010000, }, Object { - "x": 1593413268000, + "x": 1599719040000, }, Object { - "x": 1593413269000, + "x": 1599719070000, + "y": 1110000, }, Object { - "x": 1593413270000, + "x": 1599719100000, }, Object { - "x": 1593413271000, + "x": 1599719130000, }, Object { - "x": 1593413272000, + "x": 1599719160000, }, Object { - "x": 1593413273000, + "x": 1599719190000, + "y": 920000, }, Object { - "x": 1593413274000, + "x": 1599719220000, }, Object { - "x": 1593413275000, + "x": 1599719250000, }, Object { - "x": 1593413276000, + "x": 1599719280000, }, Object { - "x": 1593413277000, + "x": 1599719310000, + "y": 1074000, }, Object { - "x": 1593413278000, + "x": 1599719340000, }, Object { - "x": 1593413279000, + "x": 1599719370000, }, Object { - "x": 1593413280000, + "x": 1599719400000, }, Object { - "x": 1593413281000, + "x": 1599719430000, + "y": 902000, }, Object { - "x": 1593413282000, + "x": 1599719460000, }, Object { - "x": 1593413283000, + "x": 1599719490000, }, Object { - "x": 1593413284000, + "x": 1599719520000, }, Object { - "x": 1593413285000, + "x": 1599719550000, + "y": 931000, }, Object { - "x": 1593413286000, + "x": 1599719580000, }, Object { - "x": 1593413287000, + "x": 1599719610000, }, Object { - "x": 1593413288000, + "x": 1599719640000, }, Object { - "x": 1593413289000, + "x": 1599719670000, + "y": 1083000, }, Object { - "x": 1593413290000, + "x": 1599719700000, }, Object { - "x": 1593413291000, + "x": 1599719730000, }, Object { - "x": 1593413292000, + "x": 1599719760000, }, Object { - "x": 1593413293000, + "x": 1599719790000, + "y": 1222000, }, Object { - "x": 1593413294000, + "x": 1599719820000, }, Object { - "x": 1593413295000, + "x": 1599719850000, }, Object { - "x": 1593413296000, + "x": 1599719880000, }, Object { - "x": 1593413297000, + "x": 1599719910000, + "y": 805000, }, Object { - "x": 1593413298000, + "x": 1599719940000, }, Object { - "x": 1593413299000, + "x": 1599719970000, }, Object { - "x": 1593413300000, + "x": 1599720000000, }, Object { - "x": 1593413301000, + "x": 1599720030000, + "y": 1417000, }, Object { - "x": 1593413302000, + "x": 1599720060000, }, Object { - "x": 1593413303000, + "x": 1599720090000, }, Object { - "x": 1593413304000, + "x": 1599720120000, }, Object { - "x": 1593413305000, + "x": 1599720150000, + "y": 1122000, }, Object { - "x": 1593413306000, + "x": 1599720180000, }, Object { - "x": 1593413307000, + "x": 1599720210000, }, Object { - "x": 1593413308000, + "x": 1599720240000, + "y": 927000, }, Object { - "x": 1593413309000, + "x": 1599720270000, + "y": 1330000, }, Object { - "x": 1593413310000, + "x": 1599720300000, }, Object { - "x": 1593413311000, + "x": 1599720330000, }, Object { - "x": 1593413312000, + "x": 1599720360000, }, Object { - "x": 1593413313000, + "x": 1599720390000, + "y": 914000, }, Object { - "x": 1593413314000, + "x": 1599720420000, }, Object { - "x": 1593413315000, + "x": 1599720450000, }, Object { - "x": 1593413316000, + "x": 1599720480000, }, Object { - "x": 1593413317000, + "x": 1599720510000, + "y": 930000, }, Object { - "x": 1593413318000, + "x": 1599720540000, }, Object { - "x": 1593413319000, + "x": 1599720570000, }, Object { - "x": 1593413320000, + "x": 1599720600000, }, Object { - "x": 1593413321000, + "x": 1599720630000, + "y": 698000, }, Object { - "x": 1593413322000, + "x": 1599720660000, }, Object { - "x": 1593413323000, + "x": 1599720690000, }, Object { - "x": 1593413324000, + "x": 1599720720000, }, Object { - "x": 1593413325000, + "x": 1599720750000, + "y": 1091000, }, Object { - "x": 1593413326000, + "x": 1599720780000, }, Object { - "x": 1593413327000, + "x": 1599720810000, }, Object { - "x": 1593413328000, - "y": 77000, + "x": 1599720840000, }, Object { - "x": 1593413329000, + "x": 1599720870000, + "y": 892000, }, Object { - "x": 1593413330000, + "x": 1599720900000, }, Object { - "x": 1593413331000, + "x": 1599720930000, }, Object { - "x": 1593413332000, + "x": 1599720960000, }, Object { - "x": 1593413333000, + "x": 1599720990000, + "y": 1096000, }, Object { - "x": 1593413334000, + "x": 1599721020000, }, Object { - "x": 1593413335000, + "x": 1599721050000, }, Object { - "x": 1593413336000, + "x": 1599721080000, }, Object { - "x": 1593413337000, + "x": 1599721110000, + "y": 983000, }, Object { - "x": 1593413338000, + "x": 1599721140000, }, Object { - "x": 1593413339000, + "x": 1599721170000, }, Object { - "x": 1593413340000, + "x": 1599721200000, }, ], - "title": "HeadlessChrome", + "title": "Electron", }, ] `; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap index e204ff41dfa43..516523409a029 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap @@ -7,182 +7,1970 @@ Object { "color": "#54b399", "data": Array [ Object { - "x": 1593413100000, + "x": 1599717600000, + "y": 0.058823529411764705, + }, + Object { + "x": 1599717630000, + "y": 0.037037037037037035, + }, + Object { + "x": 1599717660000, + "y": null, + }, + Object { + "x": 1599717690000, + "y": 0.07142857142857142, + }, + Object { + "x": 1599717720000, + "y": null, + }, + Object { + "x": 1599717750000, + "y": 0.023787740164684355, + }, + Object { + "x": 1599717780000, + "y": 0.19444444444444445, + }, + Object { + "x": 1599717810000, + "y": 0.1346153846153846, + }, + Object { + "x": 1599717840000, + "y": null, + }, + Object { + "x": 1599717870000, + "y": 0.6666666666666666, + }, + Object { + "x": 1599717900000, + "y": 0.04411764705882353, + }, + Object { + "x": 1599717930000, + "y": 0.30985915492957744, + }, + Object { + "x": 1599717960000, + "y": null, + }, + Object { + "x": 1599717990000, + "y": 0.1183206106870229, + }, + Object { + "x": 1599718020000, + "y": 1, + }, + Object { + "x": 1599718050000, + "y": 0.03389830508474576, + }, + Object { + "x": 1599718080000, + "y": null, + }, + Object { + "x": 1599718110000, + "y": 0.1657754010695187, + }, + Object { + "x": 1599718140000, + "y": null, + }, + Object { + "x": 1599718170000, + "y": 0.0273972602739726, + }, + Object { + "x": 1599718200000, + "y": 0.7272727272727273, + }, + Object { + "x": 1599718230000, + "y": 0.0954356846473029, + }, + Object { + "x": 1599718260000, + "y": null, + }, + Object { + "x": 1599718290000, + "y": null, + }, + Object { + "x": 1599718320000, + "y": null, + }, + Object { + "x": 1599718350000, + "y": 0.1111111111111111, + }, + Object { + "x": 1599718380000, + "y": 0.05555555555555555, + }, + Object { + "x": 1599718410000, + "y": 0.48484848484848486, + }, + Object { + "x": 1599718440000, + "y": null, + }, + Object { + "x": 1599718470000, + "y": 0.4642857142857143, + }, + Object { + "x": 1599718500000, + "y": 0.045454545454545456, + }, + Object { + "x": 1599718530000, + "y": 1, + }, + Object { + "x": 1599718560000, + "y": null, + }, + Object { + "x": 1599718590000, + "y": 0.3469387755102041, + }, + Object { + "x": 1599718620000, + "y": null, + }, + Object { + "x": 1599718650000, + "y": 0.045454545454545456, + }, + Object { + "x": 1599718680000, + "y": null, + }, + Object { + "x": 1599718710000, + "y": 0.02967032967032967, + }, + Object { + "x": 1599718740000, + "y": 0.7910447761194029, + }, + Object { + "x": 1599718770000, + "y": 0.020833333333333332, + }, + Object { + "x": 1599718800000, + "y": 0.3448275862068966, + }, + Object { + "x": 1599718830000, + "y": null, + }, + Object { + "x": 1599718860000, + "y": 0.4107142857142857, + }, + Object { + "x": 1599718890000, + "y": 0.21428571428571427, + }, + Object { + "x": 1599718920000, + "y": null, + }, + Object { + "x": 1599718950000, + "y": 0.3235294117647059, + }, + Object { + "x": 1599718980000, + "y": null, + }, + Object { + "x": 1599719010000, + "y": null, + }, + Object { + "x": 1599719040000, + "y": 0.4642857142857143, + }, + Object { + "x": 1599719070000, + "y": 0.20192307692307693, + }, + Object { + "x": 1599719100000, + "y": 0.4, + }, + Object { + "x": 1599719130000, + "y": 0.28205128205128205, + }, + Object { + "x": 1599719160000, + "y": null, + }, + Object { + "x": 1599719190000, + "y": 0.020223152022315203, + }, + Object { + "x": 1599719220000, + "y": 0.05263157894736842, + }, + Object { + "x": 1599719250000, + "y": 0.19480519480519481, + }, + Object { + "x": 1599719280000, + "y": null, + }, + Object { + "x": 1599719310000, + "y": 0.136986301369863, + }, + Object { + "x": 1599719340000, + "y": 0.3870967741935484, + }, + Object { + "x": 1599719370000, + "y": null, + }, + Object { + "x": 1599719400000, + "y": null, + }, + Object { + "x": 1599719430000, + "y": 0.11141304347826086, + }, + Object { + "x": 1599719460000, + "y": 0.5769230769230769, + }, + Object { + "x": 1599719490000, + "y": null, + }, + Object { + "x": 1599719520000, + "y": 0.041666666666666664, + }, + Object { + "x": 1599719550000, + "y": 0.02857142857142857, + }, + Object { + "x": 1599719580000, + "y": null, + }, + Object { + "x": 1599719610000, + "y": 0.35714285714285715, + }, + Object { + "x": 1599719640000, + "y": null, + }, + Object { + "x": 1599719670000, + "y": 0.2903225806451613, + }, + Object { + "x": 1599719700000, + "y": null, + }, + Object { + "x": 1599719730000, + "y": null, + }, + Object { + "x": 1599719760000, + "y": 0.6222222222222222, + }, + Object { + "x": 1599719790000, + "y": 0.17857142857142858, + }, + Object { + "x": 1599719820000, + "y": null, + }, + Object { + "x": 1599719850000, + "y": 0.4807692307692308, + }, + Object { + "x": 1599719880000, + "y": null, + }, + Object { + "x": 1599719910000, + "y": 0.5348837209302325, + }, + Object { + "x": 1599719940000, + "y": null, + }, + Object { + "x": 1599719970000, + "y": null, + }, + Object { + "x": 1599720000000, + "y": 0.24444444444444444, + }, + Object { + "x": 1599720030000, + "y": 0.4, + }, + Object { + "x": 1599720060000, + "y": null, + }, + Object { + "x": 1599720090000, + "y": 1, + }, + Object { + "x": 1599720120000, + "y": null, + }, + Object { + "x": 1599720150000, + "y": null, + }, + Object { + "x": 1599720180000, + "y": null, + }, + Object { + "x": 1599720210000, + "y": 0.3793103448275862, + }, + Object { + "x": 1599720240000, + "y": null, + }, + Object { + "x": 1599720270000, + "y": 0.20202020202020202, + }, + Object { + "x": 1599720300000, + "y": null, + }, + Object { + "x": 1599720330000, + "y": 0.023121387283236993, + }, + Object { + "x": 1599720360000, + "y": null, + }, + Object { + "x": 1599720390000, + "y": 1, + }, + Object { + "x": 1599720420000, + "y": 0.3076923076923077, + }, + Object { + "x": 1599720450000, + "y": 0.24675324675324675, + }, + Object { + "x": 1599720480000, + "y": null, + }, + Object { + "x": 1599720510000, + "y": 0.875, + }, + Object { + "x": 1599720540000, + "y": 0.47368421052631576, + }, + Object { + "x": 1599720570000, + "y": null, + }, + Object { + "x": 1599720600000, + "y": null, + }, + Object { + "x": 1599720630000, + "y": 0.22580645161290322, + }, + Object { + "x": 1599720660000, + "y": 0.391304347826087, + }, + Object { + "x": 1599720690000, + "y": 0.75, + }, + Object { + "x": 1599720720000, + "y": null, + }, + Object { + "x": 1599720750000, + "y": 0.7804878048780488, + }, + Object { + "x": 1599720780000, + "y": 0.18518518518518517, + }, + Object { + "x": 1599720810000, + "y": 0.24074074074074073, + }, + Object { + "x": 1599720840000, + "y": null, + }, + Object { + "x": 1599720870000, + "y": 1, + }, + Object { + "x": 1599720900000, + "y": 0.1111111111111111, + }, + Object { + "x": 1599720930000, + "y": 0.5161290322580645, + }, + Object { + "x": 1599720960000, + "y": null, + }, + Object { + "x": 1599720990000, + "y": 0.14285714285714285, + }, + Object { + "x": 1599721020000, + "y": 1, + }, + Object { + "x": 1599721050000, + "y": 0.7272727272727273, + }, + Object { + "x": 1599721080000, + "y": null, + }, + Object { + "x": 1599721110000, + "y": 0.059027777777777776, + }, + Object { + "x": 1599721140000, + "y": 0.022727272727272728, + }, + Object { + "x": 1599721170000, "y": null, }, Object { - "x": 1593413130000, - "y": null, + "x": 1599721200000, + "y": null, + }, + ], + "hideLegend": false, + "legendValue": "14%", + "title": "app", + "type": "areaStacked", + }, + Object { + "color": "#6092c0", + "data": Array [ + Object { + "x": 1599717600000, + "y": 0.9411764705882353, + }, + Object { + "x": 1599717630000, + "y": 0.9629629629629629, + }, + Object { + "x": 1599717660000, + "y": null, + }, + Object { + "x": 1599717690000, + "y": 0.9285714285714286, + }, + Object { + "x": 1599717720000, + "y": null, + }, + Object { + "x": 1599717750000, + "y": 0.9734675205855444, + }, + Object { + "x": 1599717780000, + "y": 0.8055555555555556, + }, + Object { + "x": 1599717810000, + "y": 0.8653846153846154, + }, + Object { + "x": 1599717840000, + "y": null, + }, + Object { + "x": 1599717870000, + "y": 0, + }, + Object { + "x": 1599717900000, + "y": 0.9558823529411765, + }, + Object { + "x": 1599717930000, + "y": 0.6901408450704225, + }, + Object { + "x": 1599717960000, + "y": null, + }, + Object { + "x": 1599717990000, + "y": 0.7404580152671756, + }, + Object { + "x": 1599718020000, + "y": 0, + }, + Object { + "x": 1599718050000, + "y": 0.9661016949152542, + }, + Object { + "x": 1599718080000, + "y": null, + }, + Object { + "x": 1599718110000, + "y": 0.6363636363636364, + }, + Object { + "x": 1599718140000, + "y": null, + }, + Object { + "x": 1599718170000, + "y": 0.9726027397260274, + }, + Object { + "x": 1599718200000, + "y": 0, + }, + Object { + "x": 1599718230000, + "y": 0.8921161825726142, + }, + Object { + "x": 1599718260000, + "y": null, + }, + Object { + "x": 1599718290000, + "y": null, + }, + Object { + "x": 1599718320000, + "y": null, + }, + Object { + "x": 1599718350000, + "y": 0.8888888888888888, + }, + Object { + "x": 1599718380000, + "y": 0.9444444444444444, + }, + Object { + "x": 1599718410000, + "y": 0, + }, + Object { + "x": 1599718440000, + "y": null, + }, + Object { + "x": 1599718470000, + "y": 0.38392857142857145, + }, + Object { + "x": 1599718500000, + "y": 0.9545454545454546, + }, + Object { + "x": 1599718530000, + "y": 0, + }, + Object { + "x": 1599718560000, + "y": null, + }, + Object { + "x": 1599718590000, + "y": 0.3877551020408163, + }, + Object { + "x": 1599718620000, + "y": null, + }, + Object { + "x": 1599718650000, + "y": 0.9545454545454546, + }, + Object { + "x": 1599718680000, + "y": null, + }, + Object { + "x": 1599718710000, + "y": 0.9560439560439561, + }, + Object { + "x": 1599718740000, + "y": 0.208955223880597, + }, + Object { + "x": 1599718770000, + "y": 0.9791666666666666, + }, + Object { + "x": 1599718800000, + "y": 0, + }, + Object { + "x": 1599718830000, + "y": null, + }, + Object { + "x": 1599718860000, + "y": 0, + }, + Object { + "x": 1599718890000, + "y": 0.5833333333333334, + }, + Object { + "x": 1599718920000, + "y": null, + }, + Object { + "x": 1599718950000, + "y": 0.5882352941176471, + }, + Object { + "x": 1599718980000, + "y": null, + }, + Object { + "x": 1599719010000, + "y": null, + }, + Object { + "x": 1599719040000, + "y": 0.5357142857142857, + }, + Object { + "x": 1599719070000, + "y": 0.5, + }, + Object { + "x": 1599719100000, + "y": 0, + }, + Object { + "x": 1599719130000, + "y": 0.6410256410256411, + }, + Object { + "x": 1599719160000, + "y": null, + }, + Object { + "x": 1599719190000, + "y": 0.9672245467224547, + }, + Object { + "x": 1599719220000, + "y": 0.9473684210526315, + }, + Object { + "x": 1599719250000, + "y": 0.7922077922077922, + }, + Object { + "x": 1599719280000, + "y": null, + }, + Object { + "x": 1599719310000, + "y": 0.8356164383561644, + }, + Object { + "x": 1599719340000, + "y": 0.1935483870967742, + }, + Object { + "x": 1599719370000, + "y": null, + }, + Object { + "x": 1599719400000, + "y": null, + }, + Object { + "x": 1599719430000, + "y": 0.8777173913043478, + }, + Object { + "x": 1599719460000, + "y": 0.28205128205128205, + }, + Object { + "x": 1599719490000, + "y": null, + }, + Object { + "x": 1599719520000, + "y": 0.9583333333333334, + }, + Object { + "x": 1599719550000, + "y": 0.9714285714285714, + }, + Object { + "x": 1599719580000, + "y": null, + }, + Object { + "x": 1599719610000, + "y": 0, + }, + Object { + "x": 1599719640000, + "y": null, + }, + Object { + "x": 1599719670000, + "y": 0.5725806451612904, + }, + Object { + "x": 1599719700000, + "y": null, + }, + Object { + "x": 1599719730000, + "y": null, + }, + Object { + "x": 1599719760000, + "y": 0.37777777777777777, + }, + Object { + "x": 1599719790000, + "y": 0.5, + }, + Object { + "x": 1599719820000, + "y": null, + }, + Object { + "x": 1599719850000, + "y": 0.5192307692307693, + }, + Object { + "x": 1599719880000, + "y": null, + }, + Object { + "x": 1599719910000, + "y": 0.46511627906976744, + }, + Object { + "x": 1599719940000, + "y": null, + }, + Object { + "x": 1599719970000, + "y": null, + }, + Object { + "x": 1599720000000, + "y": 0.6666666666666666, + }, + Object { + "x": 1599720030000, + "y": 0.45, + }, + Object { + "x": 1599720060000, + "y": null, + }, + Object { + "x": 1599720090000, + "y": 0, + }, + Object { + "x": 1599720120000, + "y": null, + }, + Object { + "x": 1599720150000, + "y": null, + }, + Object { + "x": 1599720180000, + "y": null, + }, + Object { + "x": 1599720210000, + "y": 0, + }, + Object { + "x": 1599720240000, + "y": null, + }, + Object { + "x": 1599720270000, + "y": 0.797979797979798, + }, + Object { + "x": 1599720300000, + "y": null, + }, + Object { + "x": 1599720330000, + "y": 0.976878612716763, + }, + Object { + "x": 1599720360000, + "y": null, + }, + Object { + "x": 1599720390000, + "y": 0, + }, + Object { + "x": 1599720420000, + "y": 0.6410256410256411, + }, + Object { + "x": 1599720450000, + "y": 0.5064935064935064, + }, + Object { + "x": 1599720480000, + "y": null, + }, + Object { + "x": 1599720510000, + "y": 0, + }, + Object { + "x": 1599720540000, + "y": 0, + }, + Object { + "x": 1599720570000, + "y": null, + }, + Object { + "x": 1599720600000, + "y": null, + }, + Object { + "x": 1599720630000, + "y": 0.7258064516129032, + }, + Object { + "x": 1599720660000, + "y": 0, + }, + Object { + "x": 1599720690000, + "y": 0, }, Object { - "x": 1593413160000, + "x": 1599720720000, "y": null, }, Object { - "x": 1593413190000, - "y": null, + "x": 1599720750000, + "y": 0.21951219512195122, + }, + Object { + "x": 1599720780000, + "y": 0.6111111111111112, + }, + Object { + "x": 1599720810000, + "y": 0.6851851851851852, }, Object { - "x": 1593413220000, + "x": 1599720840000, "y": null, }, Object { - "x": 1593413250000, + "x": 1599720870000, + "y": 0, + }, + Object { + "x": 1599720900000, + "y": 0.7, + }, + Object { + "x": 1599720930000, + "y": 0.41935483870967744, + }, + Object { + "x": 1599720960000, "y": null, }, Object { - "x": 1593413280000, + "x": 1599720990000, + "y": 0.7428571428571429, + }, + Object { + "x": 1599721020000, + "y": 0, + }, + Object { + "x": 1599721050000, + "y": 0, + }, + Object { + "x": 1599721080000, "y": null, }, Object { - "x": 1593413310000, - "y": 0.16700861715223636, + "x": 1599721110000, + "y": 0.8506944444444444, + }, + Object { + "x": 1599721140000, + "y": 0.9772727272727273, + }, + Object { + "x": 1599721170000, + "y": null, }, Object { - "x": 1593413340000, + "x": 1599721200000, "y": null, }, ], "hideLegend": false, - "legendValue": "17%", - "title": "app", + "legendValue": "79%", + "title": "http", "type": "areaStacked", }, Object { - "color": "#6092c0", + "color": "#d36086", "data": Array [ Object { - "x": 1593413100000, + "x": 1599717600000, + "y": 0, + }, + Object { + "x": 1599717630000, + "y": 0, + }, + Object { + "x": 1599717660000, "y": null, }, Object { - "x": 1593413130000, + "x": 1599717690000, + "y": 0, + }, + Object { + "x": 1599717720000, "y": null, }, Object { - "x": 1593413160000, + "x": 1599717750000, + "y": 0.0027447392497712718, + }, + Object { + "x": 1599717780000, + "y": 0, + }, + Object { + "x": 1599717810000, + "y": 0, + }, + Object { + "x": 1599717840000, "y": null, }, Object { - "x": 1593413190000, + "x": 1599717870000, + "y": 0.3333333333333333, + }, + Object { + "x": 1599717900000, + "y": 0, + }, + Object { + "x": 1599717930000, + "y": 0, + }, + Object { + "x": 1599717960000, "y": null, }, Object { - "x": 1593413220000, + "x": 1599717990000, + "y": 0.14122137404580154, + }, + Object { + "x": 1599718020000, + "y": 0, + }, + Object { + "x": 1599718050000, + "y": 0, + }, + Object { + "x": 1599718080000, "y": null, }, Object { - "x": 1593413250000, + "x": 1599718110000, + "y": 0.19786096256684493, + }, + Object { + "x": 1599718140000, "y": null, }, Object { - "x": 1593413280000, + "x": 1599718170000, + "y": 0, + }, + Object { + "x": 1599718200000, + "y": 0.2727272727272727, + }, + Object { + "x": 1599718230000, + "y": 0.012448132780082987, + }, + Object { + "x": 1599718260000, "y": null, }, Object { - "x": 1593413310000, - "y": 0.7702092736971686, + "x": 1599718290000, + "y": null, }, Object { - "x": 1593413340000, + "x": 1599718320000, "y": null, }, - ], - "hideLegend": false, - "legendValue": "77%", - "title": "http", - "type": "areaStacked", - }, - Object { - "color": "#d36086", - "data": Array [ Object { - "x": 1593413100000, + "x": 1599718350000, + "y": 0, + }, + Object { + "x": 1599718380000, + "y": 0, + }, + Object { + "x": 1599718410000, + "y": 0.5151515151515151, + }, + Object { + "x": 1599718440000, + "y": null, + }, + Object { + "x": 1599718470000, + "y": 0.15178571428571427, + }, + Object { + "x": 1599718500000, + "y": 0, + }, + Object { + "x": 1599718530000, + "y": 0, + }, + Object { + "x": 1599718560000, + "y": null, + }, + Object { + "x": 1599718590000, + "y": 0.2653061224489796, + }, + Object { + "x": 1599718620000, "y": null, }, Object { - "x": 1593413130000, + "x": 1599718650000, + "y": 0, + }, + Object { + "x": 1599718680000, "y": null, }, Object { - "x": 1593413160000, + "x": 1599718710000, + "y": 0.014285714285714285, + }, + Object { + "x": 1599718740000, + "y": 0, + }, + Object { + "x": 1599718770000, + "y": 0, + }, + Object { + "x": 1599718800000, + "y": 0.6551724137931034, + }, + Object { + "x": 1599718830000, "y": null, }, Object { - "x": 1593413190000, + "x": 1599718860000, + "y": 0.5892857142857143, + }, + Object { + "x": 1599718890000, + "y": 0.20238095238095238, + }, + Object { + "x": 1599718920000, "y": null, }, Object { - "x": 1593413220000, + "x": 1599718950000, + "y": 0.08823529411764706, + }, + Object { + "x": 1599718980000, "y": null, }, Object { - "x": 1593413250000, + "x": 1599719010000, "y": null, }, Object { - "x": 1593413280000, + "x": 1599719040000, + "y": 0, + }, + Object { + "x": 1599719070000, + "y": 0.2980769230769231, + }, + Object { + "x": 1599719100000, + "y": 0.6, + }, + Object { + "x": 1599719130000, + "y": 0.07692307692307693, + }, + Object { + "x": 1599719160000, "y": null, }, Object { - "x": 1593413310000, - "y": 0.0508822322527698, + "x": 1599719190000, + "y": 0.012552301255230125, + }, + Object { + "x": 1599719220000, + "y": 0, + }, + Object { + "x": 1599719250000, + "y": 0.012987012987012988, }, Object { - "x": 1593413340000, + "x": 1599719280000, "y": null, }, - ], - "hideLegend": false, - "legendValue": "5.1%", - "title": "postgresql", - "type": "areaStacked", - }, - Object { - "color": "#9170b8", - "data": Array [ Object { - "x": 1593413100000, + "x": 1599719310000, + "y": 0.0273972602739726, + }, + Object { + "x": 1599719340000, + "y": 0.41935483870967744, + }, + Object { + "x": 1599719370000, "y": null, }, Object { - "x": 1593413130000, + "x": 1599719400000, "y": null, }, Object { - "x": 1593413160000, + "x": 1599719430000, + "y": 0.010869565217391304, + }, + Object { + "x": 1599719460000, + "y": 0.14102564102564102, + }, + Object { + "x": 1599719490000, + "y": null, + }, + Object { + "x": 1599719520000, + "y": 0, + }, + Object { + "x": 1599719550000, + "y": 0, + }, + Object { + "x": 1599719580000, + "y": null, + }, + Object { + "x": 1599719610000, + "y": 0.6428571428571429, + }, + Object { + "x": 1599719640000, + "y": null, + }, + Object { + "x": 1599719670000, + "y": 0.13709677419354838, + }, + Object { + "x": 1599719700000, + "y": null, + }, + Object { + "x": 1599719730000, + "y": null, + }, + Object { + "x": 1599719760000, + "y": 0, + }, + Object { + "x": 1599719790000, + "y": 0.32142857142857145, + }, + Object { + "x": 1599719820000, + "y": null, + }, + Object { + "x": 1599719850000, + "y": 0, + }, + Object { + "x": 1599719880000, + "y": null, + }, + Object { + "x": 1599719910000, + "y": 0, + }, + Object { + "x": 1599719940000, + "y": null, + }, + Object { + "x": 1599719970000, + "y": null, + }, + Object { + "x": 1599720000000, + "y": 0.08888888888888889, + }, + Object { + "x": 1599720030000, + "y": 0.15, + }, + Object { + "x": 1599720060000, + "y": null, + }, + Object { + "x": 1599720090000, + "y": 0, + }, + Object { + "x": 1599720120000, + "y": null, + }, + Object { + "x": 1599720150000, + "y": null, + }, + Object { + "x": 1599720180000, + "y": null, + }, + Object { + "x": 1599720210000, + "y": 0.6206896551724138, + }, + Object { + "x": 1599720240000, + "y": null, + }, + Object { + "x": 1599720270000, + "y": 0, + }, + Object { + "x": 1599720300000, + "y": null, + }, + Object { + "x": 1599720330000, + "y": 0, + }, + Object { + "x": 1599720360000, + "y": null, + }, + Object { + "x": 1599720390000, + "y": 0, + }, + Object { + "x": 1599720420000, + "y": 0.05128205128205128, + }, + Object { + "x": 1599720450000, + "y": 0.24675324675324675, + }, + Object { + "x": 1599720480000, + "y": null, + }, + Object { + "x": 1599720510000, + "y": 0.125, + }, + Object { + "x": 1599720540000, + "y": 0.5263157894736842, + }, + Object { + "x": 1599720570000, + "y": null, + }, + Object { + "x": 1599720600000, "y": null, }, Object { - "x": 1593413190000, + "x": 1599720630000, + "y": 0.04838709677419355, + }, + Object { + "x": 1599720660000, + "y": 0.6086956521739131, + }, + Object { + "x": 1599720690000, + "y": 0.25, + }, + Object { + "x": 1599720720000, "y": null, }, Object { - "x": 1593413220000, + "x": 1599720750000, + "y": 0, + }, + Object { + "x": 1599720780000, + "y": 0.2037037037037037, + }, + Object { + "x": 1599720810000, + "y": 0.07407407407407407, + }, + Object { + "x": 1599720840000, "y": null, }, Object { - "x": 1593413250000, + "x": 1599720870000, + "y": 0, + }, + Object { + "x": 1599720900000, + "y": 0.18888888888888888, + }, + Object { + "x": 1599720930000, + "y": 0.06451612903225806, + }, + Object { + "x": 1599720960000, "y": null, }, Object { - "x": 1593413280000, + "x": 1599720990000, + "y": 0.11428571428571428, + }, + Object { + "x": 1599721020000, + "y": 0, + }, + Object { + "x": 1599721050000, + "y": 0.2727272727272727, + }, + Object { + "x": 1599721080000, "y": null, }, Object { - "x": 1593413310000, - "y": 0.011899876897825195, + "x": 1599721110000, + "y": 0.09027777777777778, }, Object { - "x": 1593413340000, + "x": 1599721140000, + "y": 0, + }, + Object { + "x": 1599721170000, + "y": null, + }, + Object { + "x": 1599721200000, "y": null, }, ], "hideLegend": false, - "legendValue": "1.2%", - "title": "redis", + "legendValue": "6.4%", + "title": "postgresql", "type": "areaStacked", }, ], } `; + +exports[`Breakdown when data is loaded returns the transaction breakdown for a transaction group 9`] = ` +Array [ + Object { + "x": 1599717600000, + "y": 1, + }, + Object { + "x": 1599717630000, + "y": 1, + }, + Object { + "x": 1599717660000, + "y": null, + }, + Object { + "x": 1599717690000, + "y": 1, + }, + Object { + "x": 1599717720000, + "y": null, + }, + Object { + "x": 1599717750000, + "y": 1, + }, + Object { + "x": 1599717780000, + "y": 1, + }, + Object { + "x": 1599717810000, + "y": 1, + }, + Object { + "x": 1599717840000, + "y": null, + }, + Object { + "x": 1599717870000, + "y": null, + }, + Object { + "x": 1599717900000, + "y": 1, + }, + Object { + "x": 1599717930000, + "y": 1, + }, + Object { + "x": 1599717960000, + "y": null, + }, + Object { + "x": 1599717990000, + "y": 1, + }, + Object { + "x": 1599718020000, + "y": null, + }, + Object { + "x": 1599718050000, + "y": 1, + }, + Object { + "x": 1599718080000, + "y": null, + }, + Object { + "x": 1599718110000, + "y": 1, + }, + Object { + "x": 1599718140000, + "y": null, + }, + Object { + "x": 1599718170000, + "y": 1, + }, + Object { + "x": 1599718200000, + "y": null, + }, + Object { + "x": 1599718230000, + "y": 1, + }, + Object { + "x": 1599718260000, + "y": null, + }, + Object { + "x": 1599718290000, + "y": null, + }, + Object { + "x": 1599718320000, + "y": null, + }, + Object { + "x": 1599718350000, + "y": 1, + }, + Object { + "x": 1599718380000, + "y": 1, + }, + Object { + "x": 1599718410000, + "y": null, + }, + Object { + "x": 1599718440000, + "y": null, + }, + Object { + "x": 1599718470000, + "y": 1, + }, + Object { + "x": 1599718500000, + "y": 1, + }, + Object { + "x": 1599718530000, + "y": null, + }, + Object { + "x": 1599718560000, + "y": null, + }, + Object { + "x": 1599718590000, + "y": 1, + }, + Object { + "x": 1599718620000, + "y": null, + }, + Object { + "x": 1599718650000, + "y": 1, + }, + Object { + "x": 1599718680000, + "y": null, + }, + Object { + "x": 1599718710000, + "y": 1, + }, + Object { + "x": 1599718740000, + "y": 1, + }, + Object { + "x": 1599718770000, + "y": 1, + }, + Object { + "x": 1599718800000, + "y": null, + }, + Object { + "x": 1599718830000, + "y": null, + }, + Object { + "x": 1599718860000, + "y": null, + }, + Object { + "x": 1599718890000, + "y": 1, + }, + Object { + "x": 1599718920000, + "y": null, + }, + Object { + "x": 1599718950000, + "y": 1, + }, + Object { + "x": 1599718980000, + "y": null, + }, + Object { + "x": 1599719010000, + "y": null, + }, + Object { + "x": 1599719040000, + "y": 1, + }, + Object { + "x": 1599719070000, + "y": 1, + }, + Object { + "x": 1599719100000, + "y": null, + }, + Object { + "x": 1599719130000, + "y": 1, + }, + Object { + "x": 1599719160000, + "y": null, + }, + Object { + "x": 1599719190000, + "y": 1, + }, + Object { + "x": 1599719220000, + "y": 1, + }, + Object { + "x": 1599719250000, + "y": 1, + }, + Object { + "x": 1599719280000, + "y": null, + }, + Object { + "x": 1599719310000, + "y": 1, + }, + Object { + "x": 1599719340000, + "y": 1, + }, + Object { + "x": 1599719370000, + "y": null, + }, + Object { + "x": 1599719400000, + "y": null, + }, + Object { + "x": 1599719430000, + "y": 1, + }, + Object { + "x": 1599719460000, + "y": 1, + }, + Object { + "x": 1599719490000, + "y": null, + }, + Object { + "x": 1599719520000, + "y": 1, + }, + Object { + "x": 1599719550000, + "y": 1, + }, + Object { + "x": 1599719580000, + "y": null, + }, + Object { + "x": 1599719610000, + "y": null, + }, + Object { + "x": 1599719640000, + "y": null, + }, + Object { + "x": 1599719670000, + "y": 1, + }, + Object { + "x": 1599719700000, + "y": null, + }, + Object { + "x": 1599719730000, + "y": null, + }, + Object { + "x": 1599719760000, + "y": 1, + }, + Object { + "x": 1599719790000, + "y": 1, + }, + Object { + "x": 1599719820000, + "y": null, + }, + Object { + "x": 1599719850000, + "y": 1, + }, + Object { + "x": 1599719880000, + "y": null, + }, + Object { + "x": 1599719910000, + "y": 1, + }, + Object { + "x": 1599719940000, + "y": null, + }, + Object { + "x": 1599719970000, + "y": null, + }, + Object { + "x": 1599720000000, + "y": 1, + }, + Object { + "x": 1599720030000, + "y": 1, + }, + Object { + "x": 1599720060000, + "y": null, + }, + Object { + "x": 1599720090000, + "y": null, + }, + Object { + "x": 1599720120000, + "y": null, + }, + Object { + "x": 1599720150000, + "y": null, + }, + Object { + "x": 1599720180000, + "y": null, + }, + Object { + "x": 1599720210000, + "y": null, + }, + Object { + "x": 1599720240000, + "y": null, + }, + Object { + "x": 1599720270000, + "y": 1, + }, + Object { + "x": 1599720300000, + "y": null, + }, + Object { + "x": 1599720330000, + "y": 1, + }, + Object { + "x": 1599720360000, + "y": null, + }, + Object { + "x": 1599720390000, + "y": null, + }, + Object { + "x": 1599720420000, + "y": 1, + }, + Object { + "x": 1599720450000, + "y": 1, + }, + Object { + "x": 1599720480000, + "y": null, + }, + Object { + "x": 1599720510000, + "y": null, + }, + Object { + "x": 1599720540000, + "y": null, + }, + Object { + "x": 1599720570000, + "y": null, + }, + Object { + "x": 1599720600000, + "y": null, + }, + Object { + "x": 1599720630000, + "y": 1, + }, + Object { + "x": 1599720660000, + "y": null, + }, + Object { + "x": 1599720690000, + "y": null, + }, + Object { + "x": 1599720720000, + "y": null, + }, + Object { + "x": 1599720750000, + "y": 1, + }, + Object { + "x": 1599720780000, + "y": 1, + }, + Object { + "x": 1599720810000, + "y": 1, + }, + Object { + "x": 1599720840000, + "y": null, + }, + Object { + "x": 1599720870000, + "y": null, + }, + Object { + "x": 1599720900000, + "y": 1, + }, + Object { + "x": 1599720930000, + "y": 1, + }, + Object { + "x": 1599720960000, + "y": null, + }, + Object { + "x": 1599720990000, + "y": 1, + }, + Object { + "x": 1599721020000, + "y": null, + }, + Object { + "x": 1599721050000, + "y": null, + }, + Object { + "x": 1599721080000, + "y": null, + }, + Object { + "x": 1599721110000, + "y": 1, + }, + Object { + "x": 1599721140000, + "y": 1, + }, + Object { + "x": 1599721170000, + "y": null, + }, + Object { + "x": 1599721200000, + "y": null, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap index 16a5640c5305b..0a656d5c728a2 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap @@ -3,130 +3,95 @@ exports[`Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` Array [ Object { - "averageResponseTime": 2577, + "averageResponseTime": 2612, "impact": 0, - "key": "GET /throw-error", - "p95": 3224, - "transactionsPerMinute": 0.5, - }, - Object { - "averageResponseTime": 4757, - "impact": 0.20830834986820673, - "key": "GET /api/products/:id/customers", - "p95": 5616, - "transactionsPerMinute": 0.5, - }, - Object { - "averageResponseTime": 4749.666666666667, - "impact": 0.43453312891085794, - "key": "GET /api/orders/:id", - "p95": 7184, - "transactionsPerMinute": 0.75, + "key": "POST /api/orders", + "p95": 2608, + "transactionsPerMinute": 0.016666666666666666, }, Object { - "averageResponseTime": 8181, - "impact": 0.5354862351657939, + "averageResponseTime": 8710, + "impact": 0.21594473634705155, "key": "GET /api/types/:id", - "p95": 10080, - "transactionsPerMinute": 0.5, - }, - Object { - "averageResponseTime": 20011, - "impact": 0.7098250353192541, - "key": "POST /api", - "p95": 19968, - "transactionsPerMinute": 0.25, - }, - Object { - "averageResponseTime": 35846, - "impact": 1.466376117925459, - "key": "GET /log-error", - "p95": 35840, - "transactionsPerMinute": 0.25, + "p95": 8832, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 7105.333333333333, - "impact": 1.7905918202662048, - "key": "GET /api/stats", - "p95": 15136, - "transactionsPerMinute": 1.5, + "averageResponseTime": 15469, + "impact": 0.41307743123761353, + "key": "GET /api/products/:id/customers", + "p95": 17728, + "transactionsPerMinute": 0.03333333333333333, }, Object { - "averageResponseTime": 22958.5, - "impact": 1.9475397398343375, - "key": "GET /api/products/top", - "p95": 33216, - "transactionsPerMinute": 0.5, + "averageResponseTime": 11161.5, + "impact": 0.6129808919240927, + "key": "GET /api/customers/:id", + "p95": 16096, + "transactionsPerMinute": 0.06666666666666667, }, Object { - "averageResponseTime": 3492.9285714285716, - "impact": 2.0901067389184496, - "key": "GET static file", - "p95": 11900, - "transactionsPerMinute": 3.5, + "averageResponseTime": 8115.166666666667, + "impact": 0.6719690374213795, + "key": "GET /api/types", + "p95": 12336, + "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 26992.5, - "impact": 2.3330057413794503, - "key": "GET /api/types", - "p95": 45248, - "transactionsPerMinute": 0.5, + "averageResponseTime": 10863.6, + "impact": 0.7540274539141442, + "key": "GET /api/orders/:id", + "p95": 20192, + "transactionsPerMinute": 0.08333333333333333, }, Object { - "averageResponseTime": 13516.5, - "impact": 2.3368756900811305, + "averageResponseTime": 9906, + "impact": 0.8286631346694258, "key": "GET /api/products/:id", - "p95": 37856, - "transactionsPerMinute": 1, + "p95": 13280, + "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 8585, - "impact": 2.624924094061731, + "averageResponseTime": 8524.454545454546, + "impact": 1.329340513991638, "key": "GET /api/products", - "p95": 22112, - "transactionsPerMinute": 1.75, + "p95": 14256, + "transactionsPerMinute": 0.18333333333333332, }, Object { - "averageResponseTime": 7615.625, - "impact": 2.6645791239678345, + "averageResponseTime": 12947, + "impact": 1.472355777994578, "key": "GET /api/orders", - "p95": 11616, - "transactionsPerMinute": 2, + "p95": 25584, + "transactionsPerMinute": 0.13333333333333333, }, Object { - "averageResponseTime": 3262.95, - "impact": 2.8716452680799467, - "key": "GET /*", - "p95": 4472, - "transactionsPerMinute": 5, - }, - Object { - "averageResponseTime": 32667.5, - "impact": 2.875276331059301, - "key": "GET /log-message", - "p95": 38528, - "transactionsPerMinute": 0.5, + "averageResponseTime": 11307.75, + "impact": 2.6003199505345393, + "key": "GET /api/products/top", + "p95": 16304, + "transactionsPerMinute": 0.26666666666666666, }, Object { - "averageResponseTime": 16896.8, - "impact": 3.790160870423129, + "averageResponseTime": 26755.666666666668, + "impact": 4.644036801602961, "key": "GET /api/customers", - "p95": 26432, - "transactionsPerMinute": 1.25, + "p95": 39104, + "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 270684, - "impact": 12.686265169840583, - "key": "POST /api/orders", - "p95": 270336, - "transactionsPerMinute": 0.25, + "averageResponseTime": 17851.384615384617, + "impact": 6.730394279972759, + "key": "GET /api/stats", + "p95": 24416, + "transactionsPerMinute": 0.43333333333333335, }, Object { - "averageResponseTime": 51175.73170731707, + "averageResponseTime": 61249.30357142857, "impact": 100, "key": "GET /api", - "p95": 259040, - "transactionsPerMinute": 10.25, + "p95": 162784, + "transactionsPerMinute": 1.8666666666666667, }, ] `; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap index 0ac7741396fd4..c4b9e1ad6c931 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap @@ -1,7755 +1,3425 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Transaction charts when data is loaded returns the transaction charts 1`] = ` +exports[`Transaction charts when data is loaded returns the correct data 4`] = ` Object { "apmTimeseries": Object { - "overallAvgDuration": 38682.52419354839, + "overallAvgDuration": 578297.1431623931, "responseTimes": Object { "avg": Array [ Object { - "x": 1593413100000, - "y": null, + "x": 1599717600000, + "y": 311706, }, Object { - "x": 1593413101000, - "y": null, + "x": 1599717630000, + "y": 885570.5, }, Object { - "x": 1593413102000, - "y": null, + "x": 1599717660000, + "y": 388852.6666666667, }, Object { - "x": 1593413103000, - "y": null, + "x": 1599717690000, + "y": 503725.75, }, Object { - "x": 1593413104000, - "y": null, + "x": 1599717720000, + "y": 647100.2857142857, }, Object { - "x": 1593413105000, - "y": null, + "x": 1599717750000, + "y": 658360.4285714285, }, Object { - "x": 1593413106000, - "y": null, + "x": 1599717780000, + "y": 308246.25, }, Object { - "x": 1593413107000, - "y": null, + "x": 1599717810000, + "y": 1104229.3333333333, }, Object { - "x": 1593413108000, - "y": null, + "x": 1599717840000, + "y": 438727.6666666667, }, Object { - "x": 1593413109000, - "y": null, + "x": 1599717870000, + "y": 475918, }, Object { - "x": 1593413110000, - "y": null, + "x": 1599717900000, + "y": 254092.33333333334, }, Object { - "x": 1593413111000, - "y": null, + "x": 1599717930000, + "y": 1103846.6666666667, }, Object { - "x": 1593413112000, - "y": null, + "x": 1599717960000, + "y": 70868.8, }, Object { - "x": 1593413113000, - "y": null, + "x": 1599717990000, + "y": 384811.5, }, Object { - "x": 1593413114000, - "y": null, + "x": 1599718020000, + "y": 1139810, }, Object { - "x": 1593413115000, - "y": null, + "x": 1599718050000, + "y": 938950, }, Object { - "x": 1593413116000, - "y": null, + "x": 1599718080000, + "y": 286195.4285714286, }, Object { - "x": 1593413117000, - "y": null, + "x": 1599718110000, + "y": 373445.5, }, Object { - "x": 1593413118000, - "y": null, + "x": 1599718140000, + "y": 747249.6, }, Object { - "x": 1593413119000, - "y": null, + "x": 1599718170000, + "y": 1110552.6666666667, }, Object { - "x": 1593413120000, - "y": null, + "x": 1599718200000, + "y": 206614.66666666666, }, Object { - "x": 1593413121000, - "y": null, + "x": 1599718230000, + "y": 412294, }, Object { - "x": 1593413122000, - "y": null, + "x": 1599718260000, + "y": 643594, }, Object { - "x": 1593413123000, - "y": null, + "x": 1599718290000, + "y": 973397.5, }, Object { - "x": 1593413124000, - "y": null, + "x": 1599718320000, + "y": 521419, }, Object { - "x": 1593413125000, - "y": null, + "x": 1599718350000, + "y": 543694.3333333334, }, Object { - "x": 1593413126000, - "y": null, + "x": 1599718380000, + "y": 503212.8, }, Object { - "x": 1593413127000, - "y": null, + "x": 1599718410000, + "y": 1516441.5, }, Object { - "x": 1593413128000, - "y": null, + "x": 1599718440000, + "y": 457890.2, }, Object { - "x": 1593413129000, - "y": null, + "x": 1599718470000, + "y": 160177.55555555556, }, Object { - "x": 1593413130000, - "y": null, + "x": 1599718500000, + "y": 497083.6666666667, }, Object { - "x": 1593413131000, - "y": null, + "x": 1599718530000, + "y": 1276293, }, Object { - "x": 1593413132000, - "y": null, + "x": 1599718560000, + "y": 194131.8, }, Object { - "x": 1593413133000, - "y": null, + "x": 1599718590000, + "y": 1177525, }, Object { - "x": 1593413134000, - "y": null, + "x": 1599718620000, + "y": 504225.2, }, Object { - "x": 1593413135000, - "y": null, + "x": 1599718650000, + "y": 1397496, }, Object { - "x": 1593413136000, - "y": null, + "x": 1599718680000, + "y": 523145, }, Object { - "x": 1593413137000, - "y": null, + "x": 1599718710000, + "y": 355926.6, }, Object { - "x": 1593413138000, - "y": null, + "x": 1599718740000, + "y": 615277, }, Object { - "x": 1593413139000, - "y": null, + "x": 1599718770000, + "y": 725578, }, Object { - "x": 1593413140000, - "y": null, + "x": 1599718800000, + "y": 1721893.5, }, Object { - "x": 1593413141000, - "y": null, + "x": 1599718830000, + "y": 579859.75, }, Object { - "x": 1593413142000, - "y": null, + "x": 1599718860000, + "y": 371836.6, }, Object { - "x": 1593413143000, - "y": null, + "x": 1599718890000, + "y": 1192755.5, }, Object { - "x": 1593413144000, - "y": null, + "x": 1599718920000, + "y": 288524, }, Object { - "x": 1593413145000, - "y": null, + "x": 1599718950000, + "y": 1220730, }, Object { - "x": 1593413146000, - "y": null, + "x": 1599718980000, + "y": 591210.6666666666, }, Object { - "x": 1593413147000, - "y": null, + "x": 1599719010000, + "y": 601443.6666666666, }, Object { - "x": 1593413148000, - "y": null, + "x": 1599719040000, + "y": 225148, }, Object { - "x": 1593413149000, - "y": null, + "x": 1599719070000, + "y": 885271.5, }, Object { - "x": 1593413150000, - "y": null, + "x": 1599719100000, + "y": 578708.25, }, Object { - "x": 1593413151000, - "y": null, + "x": 1599719130000, + "y": 979768.3333333334, }, Object { - "x": 1593413152000, - "y": null, + "x": 1599719160000, + "y": 334608.71428571426, }, Object { - "x": 1593413153000, - "y": null, + "x": 1599719190000, + "y": 555571, }, Object { - "x": 1593413154000, - "y": null, + "x": 1599719220000, + "y": 249780.66666666666, }, Object { - "x": 1593413155000, + "x": 1599719250000, "y": null, }, Object { - "x": 1593413156000, - "y": null, + "x": 1599719280000, + "y": 510084, }, Object { - "x": 1593413157000, - "y": null, + "x": 1599719310000, + "y": 14228.333333333334, }, Object { - "x": 1593413158000, - "y": null, + "x": 1599719340000, + "y": 1056348.5, }, Object { - "x": 1593413159000, - "y": null, + "x": 1599719370000, + "y": 1150066, }, Object { - "x": 1593413160000, - "y": null, + "x": 1599719400000, + "y": 456110.5833333333, }, Object { - "x": 1593413161000, - "y": null, + "x": 1599719430000, + "y": 333431, }, Object { - "x": 1593413162000, - "y": null, + "x": 1599719460000, + "y": 1157360.6666666667, }, Object { - "x": 1593413163000, - "y": null, + "x": 1599719490000, + "y": 687888.75, }, Object { - "x": 1593413164000, - "y": null, + "x": 1599719520000, + "y": 574561, }, Object { - "x": 1593413165000, - "y": null, + "x": 1599719550000, + "y": 893728, }, Object { - "x": 1593413166000, - "y": null, + "x": 1599719580000, + "y": 773178.4, }, Object { - "x": 1593413167000, - "y": null, + "x": 1599719610000, + "y": 928312, }, Object { - "x": 1593413168000, - "y": null, + "x": 1599719640000, + "y": 371156.14285714284, }, Object { - "x": 1593413169000, - "y": null, + "x": 1599719670000, + "y": 464665.6666666667, }, Object { - "x": 1593413170000, + "x": 1599719700000, "y": null, }, Object { - "x": 1593413171000, - "y": null, + "x": 1599719730000, + "y": 426927.5, }, Object { - "x": 1593413172000, - "y": null, + "x": 1599719760000, + "y": 552956.7142857143, }, Object { - "x": 1593413173000, - "y": null, + "x": 1599719790000, + "y": 1099390, }, Object { - "x": 1593413174000, - "y": null, + "x": 1599719820000, + "y": 675592.75, }, Object { - "x": 1593413175000, - "y": null, + "x": 1599719850000, + "y": 1212971, }, Object { - "x": 1593413176000, - "y": null, + "x": 1599719880000, + "y": 169532.16666666666, }, Object { - "x": 1593413177000, - "y": null, + "x": 1599719910000, + "y": 1116040.6666666667, }, Object { - "x": 1593413178000, - "y": null, + "x": 1599719940000, + "y": 832511.3333333334, }, Object { - "x": 1593413179000, - "y": null, + "x": 1599719970000, + "y": 838424.75, }, Object { - "x": 1593413180000, - "y": null, + "x": 1599720000000, + "y": 705166, }, Object { - "x": 1593413181000, - "y": null, + "x": 1599720030000, + "y": 881526.3333333334, }, Object { - "x": 1593413182000, - "y": null, + "x": 1599720060000, + "y": 493560.5, }, Object { - "x": 1593413183000, - "y": null, + "x": 1599720090000, + "y": 1272019, }, Object { - "x": 1593413184000, - "y": null, + "x": 1599720120000, + "y": 1571697, }, Object { - "x": 1593413185000, - "y": null, + "x": 1599720150000, + "y": 947327.6666666666, }, Object { - "x": 1593413186000, - "y": null, + "x": 1599720180000, + "y": 770506, }, Object { - "x": 1593413187000, - "y": null, + "x": 1599720210000, + "y": 1476976, }, Object { - "x": 1593413188000, - "y": null, + "x": 1599720240000, + "y": 394579.5714285714, }, Object { - "x": 1593413189000, - "y": null, + "x": 1599720270000, + "y": 793661, }, Object { - "x": 1593413190000, - "y": null, + "x": 1599720300000, + "y": 698110.25, }, Object { - "x": 1593413191000, - "y": null, + "x": 1599720330000, + "y": 1623979, }, Object { - "x": 1593413192000, - "y": null, + "x": 1599720360000, + "y": 766940, }, Object { - "x": 1593413193000, - "y": null, + "x": 1599720390000, + "y": 516763.3, }, Object { - "x": 1593413194000, - "y": null, + "x": 1599720420000, + "y": 227079.66666666666, }, Object { - "x": 1593413195000, - "y": null, + "x": 1599720450000, + "y": 1235171, }, Object { - "x": 1593413196000, - "y": null, + "x": 1599720480000, + "y": 1110705.3333333333, }, Object { - "x": 1593413197000, - "y": null, + "x": 1599720510000, + "y": 457118, }, Object { - "x": 1593413198000, - "y": null, + "x": 1599720540000, + "y": 1113258, }, Object { - "x": 1593413199000, - "y": null, + "x": 1599720570000, + "y": 780817, }, Object { - "x": 1593413200000, - "y": null, + "x": 1599720600000, + "y": 396914.6666666667, }, Object { - "x": 1593413201000, - "y": null, + "x": 1599720630000, + "y": 323583.1666666667, }, Object { - "x": 1593413202000, - "y": null, + "x": 1599720660000, + "y": 788903.3333333334, }, Object { - "x": 1593413203000, - "y": null, + "x": 1599720690000, + "y": 339649, }, Object { - "x": 1593413204000, - "y": null, + "x": 1599720720000, + "y": 501015, }, Object { - "x": 1593413205000, - "y": null, + "x": 1599720750000, + "y": 501758.5, }, Object { - "x": 1593413206000, - "y": null, + "x": 1599720780000, + "y": 776868, }, Object { - "x": 1593413207000, - "y": null, + "x": 1599720810000, + "y": 1198145, }, Object { - "x": 1593413208000, - "y": null, + "x": 1599720840000, + "y": 561771.6666666666, }, Object { - "x": 1593413209000, - "y": null, + "x": 1599720870000, + "y": 594213.3333333334, }, Object { - "x": 1593413210000, - "y": null, + "x": 1599720900000, + "y": 188626.5, }, Object { - "x": 1593413211000, - "y": null, + "x": 1599720930000, + "y": 970632, }, Object { - "x": 1593413212000, - "y": null, + "x": 1599720960000, + "y": 750907.2, }, Object { - "x": 1593413213000, - "y": null, + "x": 1599720990000, + "y": 851925.25, }, Object { - "x": 1593413214000, - "y": null, + "x": 1599721020000, + "y": 774642.5, }, Object { - "x": 1593413215000, - "y": null, + "x": 1599721050000, + "y": 322259.5, }, Object { - "x": 1593413216000, - "y": null, + "x": 1599721080000, + "y": 369298.28571428574, }, Object { - "x": 1593413217000, - "y": null, + "x": 1599721110000, + "y": 657293.6666666666, }, Object { - "x": 1593413218000, - "y": null, + "x": 1599721140000, + "y": 479701.3333333333, }, Object { - "x": 1593413219000, - "y": null, + "x": 1599721170000, + "y": 1037489, }, Object { - "x": 1593413220000, + "x": 1599721200000, "y": null, }, + ], + "p95": Array [ Object { - "x": 1593413221000, - "y": null, + "x": 1599717600000, + "y": 483072, }, Object { - "x": 1593413222000, - "y": null, + "x": 1599717630000, + "y": 1310592, }, Object { - "x": 1593413223000, - "y": null, + "x": 1599717660000, + "y": 970688, }, Object { - "x": 1593413224000, - "y": null, + "x": 1599717690000, + "y": 814080, }, Object { - "x": 1593413225000, - "y": null, + "x": 1599717720000, + "y": 1449920, }, Object { - "x": 1593413226000, - "y": null, + "x": 1599717750000, + "y": 1302496, }, Object { - "x": 1593413227000, - "y": null, + "x": 1599717780000, + "y": 622464, }, Object { - "x": 1593413228000, - "y": null, + "x": 1599717810000, + "y": 1323008, }, Object { - "x": 1593413229000, - "y": null, + "x": 1599717840000, + "y": 1003456, }, Object { - "x": 1593413230000, - "y": null, + "x": 1599717870000, + "y": 1195904, }, Object { - "x": 1593413231000, - "y": null, + "x": 1599717900000, + "y": 692096, }, Object { - "x": 1593413232000, - "y": null, + "x": 1599717930000, + "y": 1904640, }, Object { - "x": 1593413233000, - "y": null, + "x": 1599717960000, + "y": 268224, }, Object { - "x": 1593413234000, - "y": null, + "x": 1599717990000, + "y": 1171424, }, Object { - "x": 1593413235000, - "y": null, + "x": 1599718020000, + "y": 1908480, }, Object { - "x": 1593413236000, - "y": null, + "x": 1599718050000, + "y": 937984, }, Object { - "x": 1593413237000, - "y": null, + "x": 1599718080000, + "y": 982912, }, Object { - "x": 1593413238000, - "y": null, + "x": 1599718110000, + "y": 733120, }, Object { - "x": 1593413239000, - "y": null, + "x": 1599718140000, + "y": 1736576, }, Object { - "x": 1593413240000, - "y": null, + "x": 1599718170000, + "y": 1748992, }, Object { - "x": 1593413241000, - "y": null, + "x": 1599718200000, + "y": 970720, }, Object { - "x": 1593413242000, - "y": null, + "x": 1599718230000, + "y": 1540032, }, Object { - "x": 1593413243000, - "y": null, + "x": 1599718260000, + "y": 796672, }, Object { - "x": 1593413244000, - "y": null, + "x": 1599718290000, + "y": 1134592, }, Object { - "x": 1593413245000, - "y": null, + "x": 1599718320000, + "y": 1875904, }, Object { - "x": 1593413246000, - "y": null, + "x": 1599718350000, + "y": 1253312, }, Object { - "x": 1593413247000, - "y": null, + "x": 1599718380000, + "y": 1048512, }, Object { - "x": 1593413248000, - "y": null, + "x": 1599718410000, + "y": 1654784, }, Object { - "x": 1593413249000, - "y": null, + "x": 1599718440000, + "y": 1368048, }, Object { - "x": 1593413250000, - "y": null, + "x": 1599718470000, + "y": 745456, }, Object { - "x": 1593413251000, - "y": null, + "x": 1599718500000, + "y": 1277696, }, Object { - "x": 1593413252000, - "y": null, + "x": 1599718530000, + "y": 1699840, }, Object { - "x": 1593413253000, - "y": null, + "x": 1599718560000, + "y": 1851376, }, Object { - "x": 1593413254000, - "y": null, + "x": 1599718590000, + "y": 1277952, }, Object { - "x": 1593413255000, - "y": null, + "x": 1599718620000, + "y": 1384320, }, Object { - "x": 1593413256000, - "y": null, + "x": 1599718650000, + "y": 1392640, }, Object { - "x": 1593413257000, - "y": null, + "x": 1599718680000, + "y": 1785792, }, Object { - "x": 1593413258000, - "y": null, + "x": 1599718710000, + "y": 1236928, }, Object { - "x": 1593413259000, - "y": null, + "x": 1599718740000, + "y": 1138176, }, Object { - "x": 1593413260000, - "y": null, + "x": 1599718770000, + "y": 1474432, }, Object { - "x": 1593413261000, - "y": null, + "x": 1599718800000, + "y": 1875968, }, Object { - "x": 1593413262000, - "y": null, + "x": 1599718830000, + "y": 1703920, }, Object { - "x": 1593413263000, - "y": null, + "x": 1599718860000, + "y": 1466240, }, Object { - "x": 1593413264000, - "y": null, + "x": 1599718890000, + "y": 1286144, }, Object { - "x": 1593413265000, - "y": null, + "x": 1599718920000, + "y": 831424, }, Object { - "x": 1593413266000, - "y": null, + "x": 1599718950000, + "y": 1253376, }, Object { - "x": 1593413267000, - "y": null, + "x": 1599718980000, + "y": 822272, }, Object { - "x": 1593413268000, - "y": null, + "x": 1599719010000, + "y": 1540032, }, Object { - "x": 1593413269000, - "y": null, + "x": 1599719040000, + "y": 1269728, }, Object { - "x": 1593413270000, - "y": null, + "x": 1599719070000, + "y": 1531872, }, Object { - "x": 1593413271000, - "y": null, + "x": 1599719100000, + "y": 1507264, }, Object { - "x": 1593413272000, - "y": 45093, + "x": 1599719130000, + "y": 1216512, }, Object { - "x": 1593413273000, - "y": 7498, + "x": 1599719160000, + "y": 909248, }, Object { - "x": 1593413274000, - "y": null, + "x": 1599719190000, + "y": 1097600, }, Object { - "x": 1593413275000, - "y": null, + "x": 1599719220000, + "y": 786416, }, Object { - "x": 1593413276000, + "x": 1599719250000, "y": null, }, Object { - "x": 1593413277000, - "y": 37709, + "x": 1599719280000, + "y": 1212352, }, Object { - "x": 1593413278000, - "y": null, + "x": 1599719310000, + "y": 19040, }, Object { - "x": 1593413279000, - "y": null, + "x": 1599719340000, + "y": 1077248, }, Object { - "x": 1593413280000, - "y": null, + "x": 1599719370000, + "y": 1146880, }, Object { - "x": 1593413281000, - "y": 33097, + "x": 1599719400000, + "y": 1777600, }, Object { - "x": 1593413282000, - "y": null, + "x": 1599719430000, + "y": 1261504, }, Object { - "x": 1593413283000, - "y": null, + "x": 1599719460000, + "y": 1617920, }, Object { - "x": 1593413284000, - "y": 388507, + "x": 1599719490000, + "y": 1105664, }, Object { - "x": 1593413285000, - "y": 42331.5, + "x": 1599719520000, + "y": 1163008, }, Object { - "x": 1593413286000, - "y": 99104.25, + "x": 1599719550000, + "y": 892928, }, Object { - "x": 1593413287000, - "y": 18939.5, + "x": 1599719580000, + "y": 1343360, }, Object { - "x": 1593413288000, - "y": 23229.5, + "x": 1599719610000, + "y": 925696, }, Object { - "x": 1593413289000, - "y": 11318, + "x": 1599719640000, + "y": 1277936, }, Object { - "x": 1593413290000, - "y": 15651.25, + "x": 1599719670000, + "y": 1499072, }, Object { - "x": 1593413291000, - "y": 2376, + "x": 1599719700000, + "y": null, }, Object { - "x": 1593413292000, - "y": 7796, + "x": 1599719730000, + "y": 1204096, }, Object { - "x": 1593413293000, - "y": 7571, + "x": 1599719760000, + "y": 1572800, }, Object { - "x": 1593413294000, - "y": 4219.333333333333, + "x": 1599719790000, + "y": 1097728, }, Object { - "x": 1593413295000, - "y": 6827.5, + "x": 1599719820000, + "y": 1376128, }, Object { - "x": 1593413296000, - "y": 10415.5, + "x": 1599719850000, + "y": 1277952, }, Object { - "x": 1593413297000, - "y": 10082, + "x": 1599719880000, + "y": 815072, }, Object { - "x": 1593413298000, - "y": 6459.375, + "x": 1599719910000, + "y": 1765376, }, Object { - "x": 1593413299000, - "y": 3131.5, + "x": 1599719940000, + "y": 937984, }, Object { - "x": 1593413300000, - "y": 6713.333333333333, + "x": 1599719970000, + "y": 1687488, }, Object { - "x": 1593413301000, - "y": 8800, + "x": 1599720000000, + "y": 1212352, }, Object { - "x": 1593413302000, - "y": 3743.5, + "x": 1599720030000, + "y": 1441728, }, Object { - "x": 1593413303000, - "y": 9239.5, + "x": 1599720060000, + "y": 970624, }, Object { - "x": 1593413304000, - "y": 8402, + "x": 1599720090000, + "y": 1409024, }, Object { - "x": 1593413305000, - "y": 20520.666666666668, + "x": 1599720120000, + "y": 1564672, }, Object { - "x": 1593413306000, - "y": 9319.5, + "x": 1599720150000, + "y": 1447936, }, Object { - "x": 1593413307000, - "y": 7694.333333333333, + "x": 1599720180000, + "y": 1195904, }, Object { - "x": 1593413308000, - "y": 20131, + "x": 1599720210000, + "y": 1474560, }, Object { - "x": 1593413309000, - "y": 439937.75, + "x": 1599720240000, + "y": 1220544, }, Object { - "x": 1593413310000, - "y": 11933, + "x": 1599720270000, + "y": 1261504, }, Object { - "x": 1593413311000, - "y": 18670.5, + "x": 1599720300000, + "y": 1712064, }, Object { - "x": 1593413312000, - "y": 9232, + "x": 1599720330000, + "y": 1622016, }, Object { - "x": 1593413313000, - "y": 7602, + "x": 1599720360000, + "y": 1245120, }, Object { - "x": 1593413314000, - "y": 10428.8, + "x": 1599720390000, + "y": 1646560, }, Object { - "x": 1593413315000, - "y": 8405.25, + "x": 1599720420000, + "y": 970688, }, Object { - "x": 1593413316000, - "y": 10654.5, + "x": 1599720450000, + "y": 1228800, }, Object { - "x": 1593413317000, - "y": 10250, + "x": 1599720480000, + "y": 1191936, }, Object { - "x": 1593413318000, - "y": 5775, + "x": 1599720510000, + "y": 1572832, }, Object { - "x": 1593413319000, - "y": 137867, + "x": 1599720540000, + "y": 1200128, }, Object { - "x": 1593413320000, - "y": 5694.333333333333, + "x": 1599720570000, + "y": 1071104, }, Object { - "x": 1593413321000, - "y": 6115, + "x": 1599720600000, + "y": 1130368, }, Object { - "x": 1593413322000, - "y": 1832.5, + "x": 1599720630000, + "y": 933824, }, Object { - "x": 1593413323000, - "y": null, + "x": 1599720660000, + "y": 1220544, }, Object { - "x": 1593413324000, - "y": null, + "x": 1599720690000, + "y": 337920, }, Object { - "x": 1593413325000, - "y": null, + "x": 1599720720000, + "y": 1032128, }, Object { - "x": 1593413326000, - "y": null, + "x": 1599720750000, + "y": 1736672, }, Object { - "x": 1593413327000, - "y": null, + "x": 1599720780000, + "y": 1843136, }, Object { - "x": 1593413328000, - "y": null, + "x": 1599720810000, + "y": 1404928, }, Object { - "x": 1593413329000, - "y": null, + "x": 1599720840000, + "y": 1036160, }, Object { - "x": 1593413330000, - "y": null, + "x": 1599720870000, + "y": 1351552, }, Object { - "x": 1593413331000, - "y": null, + "x": 1599720900000, + "y": 720864, }, Object { - "x": 1593413332000, - "y": null, + "x": 1599720930000, + "y": 966656, }, Object { - "x": 1593413333000, - "y": null, + "x": 1599720960000, + "y": 1892224, }, Object { - "x": 1593413334000, - "y": null, + "x": 1599720990000, + "y": 1384320, }, Object { - "x": 1593413335000, - "y": null, + "x": 1599721020000, + "y": 1540032, }, Object { - "x": 1593413336000, - "y": null, + "x": 1599721050000, + "y": 451584, }, Object { - "x": 1593413337000, - "y": null, + "x": 1599721080000, + "y": 1228768, }, Object { - "x": 1593413338000, - "y": null, + "x": 1599721110000, + "y": 927744, }, Object { - "x": 1593413339000, - "y": null, + "x": 1599721140000, + "y": 1048320, }, Object { - "x": 1593413340000, + "x": 1599721170000, + "y": 1363968, + }, + Object { + "x": 1599721200000, "y": null, }, ], - "p95": Array [ + "p99": Array [ Object { - "x": 1593413100000, - "y": null, + "x": 1599717600000, + "y": 483072, }, Object { - "x": 1593413101000, - "y": null, + "x": 1599717630000, + "y": 1310592, }, Object { - "x": 1593413102000, - "y": null, + "x": 1599717660000, + "y": 970688, }, Object { - "x": 1593413103000, - "y": null, + "x": 1599717690000, + "y": 814080, }, Object { - "x": 1593413104000, - "y": null, + "x": 1599717720000, + "y": 1449920, }, Object { - "x": 1593413105000, - "y": null, + "x": 1599717750000, + "y": 1302496, }, Object { - "x": 1593413106000, - "y": null, + "x": 1599717780000, + "y": 622464, }, Object { - "x": 1593413107000, - "y": null, + "x": 1599717810000, + "y": 1323008, }, Object { - "x": 1593413108000, - "y": null, + "x": 1599717840000, + "y": 1003456, }, Object { - "x": 1593413109000, - "y": null, + "x": 1599717870000, + "y": 1195904, }, Object { - "x": 1593413110000, - "y": null, + "x": 1599717900000, + "y": 692096, }, Object { - "x": 1593413111000, - "y": null, + "x": 1599717930000, + "y": 1904640, }, Object { - "x": 1593413112000, - "y": null, + "x": 1599717960000, + "y": 268224, }, Object { - "x": 1593413113000, - "y": null, + "x": 1599717990000, + "y": 1171424, }, Object { - "x": 1593413114000, - "y": null, + "x": 1599718020000, + "y": 1908480, }, Object { - "x": 1593413115000, - "y": null, + "x": 1599718050000, + "y": 937984, }, Object { - "x": 1593413116000, - "y": null, + "x": 1599718080000, + "y": 982912, }, Object { - "x": 1593413117000, - "y": null, + "x": 1599718110000, + "y": 733120, }, Object { - "x": 1593413118000, - "y": null, + "x": 1599718140000, + "y": 1736576, }, Object { - "x": 1593413119000, - "y": null, + "x": 1599718170000, + "y": 1748992, }, Object { - "x": 1593413120000, - "y": null, + "x": 1599718200000, + "y": 970720, }, Object { - "x": 1593413121000, - "y": null, + "x": 1599718230000, + "y": 1540032, }, Object { - "x": 1593413122000, - "y": null, + "x": 1599718260000, + "y": 796672, }, Object { - "x": 1593413123000, - "y": null, + "x": 1599718290000, + "y": 1134592, }, Object { - "x": 1593413124000, - "y": null, + "x": 1599718320000, + "y": 1875904, }, Object { - "x": 1593413125000, - "y": null, + "x": 1599718350000, + "y": 1253312, }, Object { - "x": 1593413126000, - "y": null, + "x": 1599718380000, + "y": 1048512, }, Object { - "x": 1593413127000, - "y": null, + "x": 1599718410000, + "y": 1654784, }, Object { - "x": 1593413128000, - "y": null, + "x": 1599718440000, + "y": 1368048, }, Object { - "x": 1593413129000, - "y": null, + "x": 1599718470000, + "y": 745456, }, Object { - "x": 1593413130000, - "y": null, + "x": 1599718500000, + "y": 1277696, }, Object { - "x": 1593413131000, - "y": null, + "x": 1599718530000, + "y": 1699840, }, Object { - "x": 1593413132000, - "y": null, + "x": 1599718560000, + "y": 1851376, }, Object { - "x": 1593413133000, - "y": null, + "x": 1599718590000, + "y": 1277952, }, Object { - "x": 1593413134000, - "y": null, + "x": 1599718620000, + "y": 1384320, }, Object { - "x": 1593413135000, - "y": null, + "x": 1599718650000, + "y": 1392640, }, Object { - "x": 1593413136000, - "y": null, + "x": 1599718680000, + "y": 1785792, }, Object { - "x": 1593413137000, - "y": null, + "x": 1599718710000, + "y": 1236928, }, Object { - "x": 1593413138000, - "y": null, + "x": 1599718740000, + "y": 1138176, }, Object { - "x": 1593413139000, - "y": null, + "x": 1599718770000, + "y": 1474432, }, Object { - "x": 1593413140000, - "y": null, + "x": 1599718800000, + "y": 1875968, }, Object { - "x": 1593413141000, - "y": null, + "x": 1599718830000, + "y": 1703920, }, Object { - "x": 1593413142000, - "y": null, + "x": 1599718860000, + "y": 1466240, }, Object { - "x": 1593413143000, - "y": null, + "x": 1599718890000, + "y": 1286144, }, Object { - "x": 1593413144000, - "y": null, + "x": 1599718920000, + "y": 831424, }, Object { - "x": 1593413145000, - "y": null, + "x": 1599718950000, + "y": 1253376, }, Object { - "x": 1593413146000, - "y": null, + "x": 1599718980000, + "y": 822272, }, Object { - "x": 1593413147000, - "y": null, + "x": 1599719010000, + "y": 1540032, }, Object { - "x": 1593413148000, - "y": null, + "x": 1599719040000, + "y": 1269728, }, Object { - "x": 1593413149000, - "y": null, + "x": 1599719070000, + "y": 1531872, }, Object { - "x": 1593413150000, - "y": null, + "x": 1599719100000, + "y": 1507264, }, Object { - "x": 1593413151000, - "y": null, + "x": 1599719130000, + "y": 1216512, }, Object { - "x": 1593413152000, - "y": null, + "x": 1599719160000, + "y": 909248, }, Object { - "x": 1593413153000, - "y": null, + "x": 1599719190000, + "y": 1097600, }, Object { - "x": 1593413154000, - "y": null, + "x": 1599719220000, + "y": 786416, }, Object { - "x": 1593413155000, + "x": 1599719250000, "y": null, }, Object { - "x": 1593413156000, - "y": null, + "x": 1599719280000, + "y": 1212352, }, Object { - "x": 1593413157000, - "y": null, - }, - Object { - "x": 1593413158000, - "y": null, - }, - Object { - "x": 1593413159000, - "y": null, - }, - Object { - "x": 1593413160000, - "y": null, - }, - Object { - "x": 1593413161000, - "y": null, - }, - Object { - "x": 1593413162000, - "y": null, - }, - Object { - "x": 1593413163000, - "y": null, - }, - Object { - "x": 1593413164000, - "y": null, - }, - Object { - "x": 1593413165000, - "y": null, - }, - Object { - "x": 1593413166000, - "y": null, - }, - Object { - "x": 1593413167000, - "y": null, - }, - Object { - "x": 1593413168000, - "y": null, - }, - Object { - "x": 1593413169000, - "y": null, - }, - Object { - "x": 1593413170000, - "y": null, - }, - Object { - "x": 1593413171000, - "y": null, - }, - Object { - "x": 1593413172000, - "y": null, - }, - Object { - "x": 1593413173000, - "y": null, - }, - Object { - "x": 1593413174000, - "y": null, - }, - Object { - "x": 1593413175000, - "y": null, - }, - Object { - "x": 1593413176000, - "y": null, - }, - Object { - "x": 1593413177000, - "y": null, - }, - Object { - "x": 1593413178000, - "y": null, - }, - Object { - "x": 1593413179000, - "y": null, - }, - Object { - "x": 1593413180000, - "y": null, - }, - Object { - "x": 1593413181000, - "y": null, - }, - Object { - "x": 1593413182000, - "y": null, - }, - Object { - "x": 1593413183000, - "y": null, - }, - Object { - "x": 1593413184000, - "y": null, - }, - Object { - "x": 1593413185000, - "y": null, - }, - Object { - "x": 1593413186000, - "y": null, - }, - Object { - "x": 1593413187000, - "y": null, - }, - Object { - "x": 1593413188000, - "y": null, - }, - Object { - "x": 1593413189000, - "y": null, - }, - Object { - "x": 1593413190000, - "y": null, - }, - Object { - "x": 1593413191000, - "y": null, - }, - Object { - "x": 1593413192000, - "y": null, - }, - Object { - "x": 1593413193000, - "y": null, - }, - Object { - "x": 1593413194000, - "y": null, - }, - Object { - "x": 1593413195000, - "y": null, - }, - Object { - "x": 1593413196000, - "y": null, - }, - Object { - "x": 1593413197000, - "y": null, - }, - Object { - "x": 1593413198000, - "y": null, - }, - Object { - "x": 1593413199000, - "y": null, - }, - Object { - "x": 1593413200000, - "y": null, - }, - Object { - "x": 1593413201000, - "y": null, - }, - Object { - "x": 1593413202000, - "y": null, - }, - Object { - "x": 1593413203000, - "y": null, - }, - Object { - "x": 1593413204000, - "y": null, - }, - Object { - "x": 1593413205000, - "y": null, - }, - Object { - "x": 1593413206000, - "y": null, - }, - Object { - "x": 1593413207000, - "y": null, - }, - Object { - "x": 1593413208000, - "y": null, - }, - Object { - "x": 1593413209000, - "y": null, - }, - Object { - "x": 1593413210000, - "y": null, - }, - Object { - "x": 1593413211000, - "y": null, - }, - Object { - "x": 1593413212000, - "y": null, - }, - Object { - "x": 1593413213000, - "y": null, - }, - Object { - "x": 1593413214000, - "y": null, - }, - Object { - "x": 1593413215000, - "y": null, - }, - Object { - "x": 1593413216000, - "y": null, - }, - Object { - "x": 1593413217000, - "y": null, - }, - Object { - "x": 1593413218000, - "y": null, - }, - Object { - "x": 1593413219000, - "y": null, - }, - Object { - "x": 1593413220000, - "y": null, - }, - Object { - "x": 1593413221000, - "y": null, - }, - Object { - "x": 1593413222000, - "y": null, - }, - Object { - "x": 1593413223000, - "y": null, - }, - Object { - "x": 1593413224000, - "y": null, + "x": 1599719310000, + "y": 19040, }, Object { - "x": 1593413225000, - "y": null, + "x": 1599719340000, + "y": 1077248, }, Object { - "x": 1593413226000, - "y": null, + "x": 1599719370000, + "y": 1146880, }, Object { - "x": 1593413227000, - "y": null, + "x": 1599719400000, + "y": 1908672, }, Object { - "x": 1593413228000, - "y": null, + "x": 1599719430000, + "y": 1261504, }, Object { - "x": 1593413229000, - "y": null, + "x": 1599719460000, + "y": 1617920, }, Object { - "x": 1593413230000, - "y": null, + "x": 1599719490000, + "y": 1105664, }, Object { - "x": 1593413231000, - "y": null, + "x": 1599719520000, + "y": 1163008, }, Object { - "x": 1593413232000, - "y": null, + "x": 1599719550000, + "y": 892928, }, Object { - "x": 1593413233000, - "y": null, + "x": 1599719580000, + "y": 1343360, }, Object { - "x": 1593413234000, - "y": null, + "x": 1599719610000, + "y": 925696, }, Object { - "x": 1593413235000, - "y": null, + "x": 1599719640000, + "y": 1277936, }, Object { - "x": 1593413236000, - "y": null, + "x": 1599719670000, + "y": 1499072, }, Object { - "x": 1593413237000, + "x": 1599719700000, "y": null, }, Object { - "x": 1593413238000, - "y": null, + "x": 1599719730000, + "y": 1204096, }, Object { - "x": 1593413239000, - "y": null, + "x": 1599719760000, + "y": 1572800, }, Object { - "x": 1593413240000, - "y": null, + "x": 1599719790000, + "y": 1097728, }, Object { - "x": 1593413241000, - "y": null, + "x": 1599719820000, + "y": 1376128, }, Object { - "x": 1593413242000, - "y": null, + "x": 1599719850000, + "y": 1277952, }, Object { - "x": 1593413243000, - "y": null, + "x": 1599719880000, + "y": 815072, }, Object { - "x": 1593413244000, - "y": null, + "x": 1599719910000, + "y": 1765376, }, Object { - "x": 1593413245000, - "y": null, + "x": 1599719940000, + "y": 937984, }, Object { - "x": 1593413246000, - "y": null, + "x": 1599719970000, + "y": 1687488, }, Object { - "x": 1593413247000, - "y": null, + "x": 1599720000000, + "y": 1212352, }, Object { - "x": 1593413248000, - "y": null, + "x": 1599720030000, + "y": 1441728, }, Object { - "x": 1593413249000, - "y": null, + "x": 1599720060000, + "y": 970624, }, Object { - "x": 1593413250000, - "y": null, + "x": 1599720090000, + "y": 1409024, }, Object { - "x": 1593413251000, - "y": null, + "x": 1599720120000, + "y": 1564672, }, Object { - "x": 1593413252000, - "y": null, + "x": 1599720150000, + "y": 1447936, }, Object { - "x": 1593413253000, - "y": null, + "x": 1599720180000, + "y": 1195904, }, Object { - "x": 1593413254000, - "y": null, + "x": 1599720210000, + "y": 1474560, }, Object { - "x": 1593413255000, - "y": null, + "x": 1599720240000, + "y": 1220544, }, Object { - "x": 1593413256000, - "y": null, + "x": 1599720270000, + "y": 1261504, }, Object { - "x": 1593413257000, - "y": null, + "x": 1599720300000, + "y": 1712064, }, Object { - "x": 1593413258000, - "y": null, + "x": 1599720330000, + "y": 1622016, }, Object { - "x": 1593413259000, - "y": null, + "x": 1599720360000, + "y": 1245120, }, Object { - "x": 1593413260000, - "y": null, + "x": 1599720390000, + "y": 1646560, }, Object { - "x": 1593413261000, - "y": null, + "x": 1599720420000, + "y": 970688, }, Object { - "x": 1593413262000, - "y": null, + "x": 1599720450000, + "y": 1228800, }, Object { - "x": 1593413263000, - "y": null, + "x": 1599720480000, + "y": 1191936, }, Object { - "x": 1593413264000, - "y": null, + "x": 1599720510000, + "y": 1572832, }, Object { - "x": 1593413265000, - "y": null, + "x": 1599720540000, + "y": 1200128, }, Object { - "x": 1593413266000, - "y": null, + "x": 1599720570000, + "y": 1071104, }, Object { - "x": 1593413267000, - "y": null, + "x": 1599720600000, + "y": 1130368, }, Object { - "x": 1593413268000, - "y": null, + "x": 1599720630000, + "y": 933824, }, Object { - "x": 1593413269000, - "y": null, + "x": 1599720660000, + "y": 1220544, }, Object { - "x": 1593413270000, - "y": null, + "x": 1599720690000, + "y": 337920, }, Object { - "x": 1593413271000, - "y": null, + "x": 1599720720000, + "y": 1032128, }, Object { - "x": 1593413272000, - "y": 45056, + "x": 1599720750000, + "y": 1736672, }, Object { - "x": 1593413273000, - "y": 10080, + "x": 1599720780000, + "y": 1843136, }, Object { - "x": 1593413274000, - "y": null, + "x": 1599720810000, + "y": 1404928, }, Object { - "x": 1593413275000, - "y": null, + "x": 1599720840000, + "y": 1036160, }, Object { - "x": 1593413276000, - "y": null, + "x": 1599720870000, + "y": 1351552, }, Object { - "x": 1593413277000, - "y": 37632, + "x": 1599720900000, + "y": 720864, }, Object { - "x": 1593413278000, - "y": null, + "x": 1599720930000, + "y": 966656, }, Object { - "x": 1593413279000, - "y": null, + "x": 1599720960000, + "y": 1892224, }, Object { - "x": 1593413280000, - "y": null, + "x": 1599720990000, + "y": 1384320, }, Object { - "x": 1593413281000, - "y": 33024, + "x": 1599721020000, + "y": 1540032, }, Object { - "x": 1593413282000, - "y": null, + "x": 1599721050000, + "y": 451584, }, Object { - "x": 1593413283000, - "y": null, + "x": 1599721080000, + "y": 1228768, }, Object { - "x": 1593413284000, - "y": 761728, - }, - Object { - "x": 1593413285000, - "y": 81904, - }, - Object { - "x": 1593413286000, - "y": 358384, - }, - Object { - "x": 1593413287000, - "y": 36088, - }, - Object { - "x": 1593413288000, - "y": 44536, - }, - Object { - "x": 1593413289000, - "y": 11648, - }, - Object { - "x": 1593413290000, - "y": 31984, - }, - Object { - "x": 1593413291000, - "y": 2920, - }, - Object { - "x": 1593413292000, - "y": 9312, - }, - Object { - "x": 1593413293000, - "y": 10912, - }, - Object { - "x": 1593413294000, - "y": 6392, - }, - Object { - "x": 1593413295000, - "y": 11704, - }, - Object { - "x": 1593413296000, - "y": 10816, - }, - Object { - "x": 1593413297000, - "y": 12000, - }, - Object { - "x": 1593413298000, - "y": 15164, - }, - Object { - "x": 1593413299000, - "y": 3216, - }, - Object { - "x": 1593413300000, - "y": 9584, - }, - Object { - "x": 1593413301000, - "y": 21240, - }, - Object { - "x": 1593413302000, - "y": 5624, - }, - Object { - "x": 1593413303000, - "y": 11360, - }, - Object { - "x": 1593413304000, - "y": 12320, - }, - Object { - "x": 1593413305000, - "y": 38640, - }, - Object { - "x": 1593413306000, - "y": 9728, - }, - Object { - "x": 1593413307000, - "y": 17016, - }, - Object { - "x": 1593413308000, - "y": 26848, - }, - Object { - "x": 1593413309000, - "y": 1753072, - }, - Object { - "x": 1593413310000, - "y": 16992, - }, - Object { - "x": 1593413311000, - "y": 26560, - }, - Object { - "x": 1593413312000, - "y": 11232, - }, - Object { - "x": 1593413313000, - "y": 11424, - }, - Object { - "x": 1593413314000, - "y": 16096, - }, - Object { - "x": 1593413315000, - "y": 18800, - }, - Object { - "x": 1593413316000, - "y": 12672, - }, - Object { - "x": 1593413317000, - "y": 24316, - }, - Object { - "x": 1593413318000, - "y": 8944, - }, - Object { - "x": 1593413319000, - "y": 272352, - }, - Object { - "x": 1593413320000, - "y": 7992, - }, - Object { - "x": 1593413321000, - "y": 8368, - }, - Object { - "x": 1593413322000, - "y": 1928, - }, - Object { - "x": 1593413323000, - "y": null, - }, - Object { - "x": 1593413324000, - "y": null, - }, - Object { - "x": 1593413325000, - "y": null, - }, - Object { - "x": 1593413326000, - "y": null, - }, - Object { - "x": 1593413327000, - "y": null, - }, - Object { - "x": 1593413328000, - "y": null, - }, - Object { - "x": 1593413329000, - "y": null, - }, - Object { - "x": 1593413330000, - "y": null, - }, - Object { - "x": 1593413331000, - "y": null, - }, - Object { - "x": 1593413332000, - "y": null, - }, - Object { - "x": 1593413333000, - "y": null, - }, - Object { - "x": 1593413334000, - "y": null, - }, - Object { - "x": 1593413335000, - "y": null, - }, - Object { - "x": 1593413336000, - "y": null, - }, - Object { - "x": 1593413337000, - "y": null, - }, - Object { - "x": 1593413338000, - "y": null, - }, - Object { - "x": 1593413339000, - "y": null, - }, - Object { - "x": 1593413340000, - "y": null, - }, - ], - "p99": Array [ - Object { - "x": 1593413100000, - "y": null, - }, - Object { - "x": 1593413101000, - "y": null, - }, - Object { - "x": 1593413102000, - "y": null, - }, - Object { - "x": 1593413103000, - "y": null, - }, - Object { - "x": 1593413104000, - "y": null, - }, - Object { - "x": 1593413105000, - "y": null, - }, - Object { - "x": 1593413106000, - "y": null, - }, - Object { - "x": 1593413107000, - "y": null, - }, - Object { - "x": 1593413108000, - "y": null, - }, - Object { - "x": 1593413109000, - "y": null, - }, - Object { - "x": 1593413110000, - "y": null, - }, - Object { - "x": 1593413111000, - "y": null, - }, - Object { - "x": 1593413112000, - "y": null, - }, - Object { - "x": 1593413113000, - "y": null, - }, - Object { - "x": 1593413114000, - "y": null, - }, - Object { - "x": 1593413115000, - "y": null, - }, - Object { - "x": 1593413116000, - "y": null, - }, - Object { - "x": 1593413117000, - "y": null, - }, - Object { - "x": 1593413118000, - "y": null, - }, - Object { - "x": 1593413119000, - "y": null, - }, - Object { - "x": 1593413120000, - "y": null, - }, - Object { - "x": 1593413121000, - "y": null, - }, - Object { - "x": 1593413122000, - "y": null, - }, - Object { - "x": 1593413123000, - "y": null, - }, - Object { - "x": 1593413124000, - "y": null, - }, - Object { - "x": 1593413125000, - "y": null, - }, - Object { - "x": 1593413126000, - "y": null, - }, - Object { - "x": 1593413127000, - "y": null, - }, - Object { - "x": 1593413128000, - "y": null, - }, - Object { - "x": 1593413129000, - "y": null, - }, - Object { - "x": 1593413130000, - "y": null, - }, - Object { - "x": 1593413131000, - "y": null, - }, - Object { - "x": 1593413132000, - "y": null, - }, - Object { - "x": 1593413133000, - "y": null, - }, - Object { - "x": 1593413134000, - "y": null, - }, - Object { - "x": 1593413135000, - "y": null, - }, - Object { - "x": 1593413136000, - "y": null, - }, - Object { - "x": 1593413137000, - "y": null, - }, - Object { - "x": 1593413138000, - "y": null, - }, - Object { - "x": 1593413139000, - "y": null, - }, - Object { - "x": 1593413140000, - "y": null, - }, - Object { - "x": 1593413141000, - "y": null, - }, - Object { - "x": 1593413142000, - "y": null, - }, - Object { - "x": 1593413143000, - "y": null, - }, - Object { - "x": 1593413144000, - "y": null, - }, - Object { - "x": 1593413145000, - "y": null, - }, - Object { - "x": 1593413146000, - "y": null, - }, - Object { - "x": 1593413147000, - "y": null, - }, - Object { - "x": 1593413148000, - "y": null, - }, - Object { - "x": 1593413149000, - "y": null, - }, - Object { - "x": 1593413150000, - "y": null, - }, - Object { - "x": 1593413151000, - "y": null, - }, - Object { - "x": 1593413152000, - "y": null, - }, - Object { - "x": 1593413153000, - "y": null, - }, - Object { - "x": 1593413154000, - "y": null, - }, - Object { - "x": 1593413155000, - "y": null, - }, - Object { - "x": 1593413156000, - "y": null, - }, - Object { - "x": 1593413157000, - "y": null, - }, - Object { - "x": 1593413158000, - "y": null, - }, - Object { - "x": 1593413159000, - "y": null, - }, - Object { - "x": 1593413160000, - "y": null, - }, - Object { - "x": 1593413161000, - "y": null, - }, - Object { - "x": 1593413162000, - "y": null, - }, - Object { - "x": 1593413163000, - "y": null, - }, - Object { - "x": 1593413164000, - "y": null, - }, - Object { - "x": 1593413165000, - "y": null, - }, - Object { - "x": 1593413166000, - "y": null, - }, - Object { - "x": 1593413167000, - "y": null, - }, - Object { - "x": 1593413168000, - "y": null, - }, - Object { - "x": 1593413169000, - "y": null, - }, - Object { - "x": 1593413170000, - "y": null, - }, - Object { - "x": 1593413171000, - "y": null, - }, - Object { - "x": 1593413172000, - "y": null, - }, - Object { - "x": 1593413173000, - "y": null, - }, - Object { - "x": 1593413174000, - "y": null, - }, - Object { - "x": 1593413175000, - "y": null, - }, - Object { - "x": 1593413176000, - "y": null, - }, - Object { - "x": 1593413177000, - "y": null, - }, - Object { - "x": 1593413178000, - "y": null, - }, - Object { - "x": 1593413179000, - "y": null, - }, - Object { - "x": 1593413180000, - "y": null, - }, - Object { - "x": 1593413181000, - "y": null, - }, - Object { - "x": 1593413182000, - "y": null, - }, - Object { - "x": 1593413183000, - "y": null, - }, - Object { - "x": 1593413184000, - "y": null, - }, - Object { - "x": 1593413185000, - "y": null, - }, - Object { - "x": 1593413186000, - "y": null, - }, - Object { - "x": 1593413187000, - "y": null, - }, - Object { - "x": 1593413188000, - "y": null, - }, - Object { - "x": 1593413189000, - "y": null, - }, - Object { - "x": 1593413190000, - "y": null, - }, - Object { - "x": 1593413191000, - "y": null, - }, - Object { - "x": 1593413192000, - "y": null, - }, - Object { - "x": 1593413193000, - "y": null, - }, - Object { - "x": 1593413194000, - "y": null, - }, - Object { - "x": 1593413195000, - "y": null, - }, - Object { - "x": 1593413196000, - "y": null, - }, - Object { - "x": 1593413197000, - "y": null, - }, - Object { - "x": 1593413198000, - "y": null, - }, - Object { - "x": 1593413199000, - "y": null, - }, - Object { - "x": 1593413200000, - "y": null, - }, - Object { - "x": 1593413201000, - "y": null, - }, - Object { - "x": 1593413202000, - "y": null, - }, - Object { - "x": 1593413203000, - "y": null, - }, - Object { - "x": 1593413204000, - "y": null, - }, - Object { - "x": 1593413205000, - "y": null, - }, - Object { - "x": 1593413206000, - "y": null, - }, - Object { - "x": 1593413207000, - "y": null, - }, - Object { - "x": 1593413208000, - "y": null, - }, - Object { - "x": 1593413209000, - "y": null, - }, - Object { - "x": 1593413210000, - "y": null, - }, - Object { - "x": 1593413211000, - "y": null, - }, - Object { - "x": 1593413212000, - "y": null, - }, - Object { - "x": 1593413213000, - "y": null, - }, - Object { - "x": 1593413214000, - "y": null, - }, - Object { - "x": 1593413215000, - "y": null, - }, - Object { - "x": 1593413216000, - "y": null, - }, - Object { - "x": 1593413217000, - "y": null, - }, - Object { - "x": 1593413218000, - "y": null, - }, - Object { - "x": 1593413219000, - "y": null, - }, - Object { - "x": 1593413220000, - "y": null, - }, - Object { - "x": 1593413221000, - "y": null, - }, - Object { - "x": 1593413222000, - "y": null, - }, - Object { - "x": 1593413223000, - "y": null, - }, - Object { - "x": 1593413224000, - "y": null, - }, - Object { - "x": 1593413225000, - "y": null, - }, - Object { - "x": 1593413226000, - "y": null, - }, - Object { - "x": 1593413227000, - "y": null, - }, - Object { - "x": 1593413228000, - "y": null, - }, - Object { - "x": 1593413229000, - "y": null, - }, - Object { - "x": 1593413230000, - "y": null, - }, - Object { - "x": 1593413231000, - "y": null, - }, - Object { - "x": 1593413232000, - "y": null, - }, - Object { - "x": 1593413233000, - "y": null, - }, - Object { - "x": 1593413234000, - "y": null, - }, - Object { - "x": 1593413235000, - "y": null, - }, - Object { - "x": 1593413236000, - "y": null, - }, - Object { - "x": 1593413237000, - "y": null, - }, - Object { - "x": 1593413238000, - "y": null, - }, - Object { - "x": 1593413239000, - "y": null, - }, - Object { - "x": 1593413240000, - "y": null, - }, - Object { - "x": 1593413241000, - "y": null, - }, - Object { - "x": 1593413242000, - "y": null, - }, - Object { - "x": 1593413243000, - "y": null, - }, - Object { - "x": 1593413244000, - "y": null, - }, - Object { - "x": 1593413245000, - "y": null, - }, - Object { - "x": 1593413246000, - "y": null, - }, - Object { - "x": 1593413247000, - "y": null, - }, - Object { - "x": 1593413248000, - "y": null, - }, - Object { - "x": 1593413249000, - "y": null, - }, - Object { - "x": 1593413250000, - "y": null, - }, - Object { - "x": 1593413251000, - "y": null, - }, - Object { - "x": 1593413252000, - "y": null, - }, - Object { - "x": 1593413253000, - "y": null, - }, - Object { - "x": 1593413254000, - "y": null, - }, - Object { - "x": 1593413255000, - "y": null, - }, - Object { - "x": 1593413256000, - "y": null, - }, - Object { - "x": 1593413257000, - "y": null, - }, - Object { - "x": 1593413258000, - "y": null, - }, - Object { - "x": 1593413259000, - "y": null, - }, - Object { - "x": 1593413260000, - "y": null, - }, - Object { - "x": 1593413261000, - "y": null, - }, - Object { - "x": 1593413262000, - "y": null, - }, - Object { - "x": 1593413263000, - "y": null, - }, - Object { - "x": 1593413264000, - "y": null, - }, - Object { - "x": 1593413265000, - "y": null, - }, - Object { - "x": 1593413266000, - "y": null, - }, - Object { - "x": 1593413267000, - "y": null, - }, - Object { - "x": 1593413268000, - "y": null, - }, - Object { - "x": 1593413269000, - "y": null, - }, - Object { - "x": 1593413270000, - "y": null, - }, - Object { - "x": 1593413271000, - "y": null, - }, - Object { - "x": 1593413272000, - "y": 45056, - }, - Object { - "x": 1593413273000, - "y": 10080, - }, - Object { - "x": 1593413274000, - "y": null, - }, - Object { - "x": 1593413275000, - "y": null, - }, - Object { - "x": 1593413276000, - "y": null, - }, - Object { - "x": 1593413277000, - "y": 37632, - }, - Object { - "x": 1593413278000, - "y": null, - }, - Object { - "x": 1593413279000, - "y": null, - }, - Object { - "x": 1593413280000, - "y": null, - }, - Object { - "x": 1593413281000, - "y": 33024, - }, - Object { - "x": 1593413282000, - "y": null, - }, - Object { - "x": 1593413283000, - "y": null, - }, - Object { - "x": 1593413284000, - "y": 761728, - }, - Object { - "x": 1593413285000, - "y": 81904, - }, - Object { - "x": 1593413286000, - "y": 358384, - }, - Object { - "x": 1593413287000, - "y": 36088, - }, - Object { - "x": 1593413288000, - "y": 44536, - }, - Object { - "x": 1593413289000, - "y": 11648, - }, - Object { - "x": 1593413290000, - "y": 31984, - }, - Object { - "x": 1593413291000, - "y": 2920, - }, - Object { - "x": 1593413292000, - "y": 9312, - }, - Object { - "x": 1593413293000, - "y": 10912, - }, - Object { - "x": 1593413294000, - "y": 6392, - }, - Object { - "x": 1593413295000, - "y": 11704, - }, - Object { - "x": 1593413296000, - "y": 10816, - }, - Object { - "x": 1593413297000, - "y": 12000, - }, - Object { - "x": 1593413298000, - "y": 15164, - }, - Object { - "x": 1593413299000, - "y": 3216, - }, - Object { - "x": 1593413300000, - "y": 9584, - }, - Object { - "x": 1593413301000, - "y": 21240, - }, - Object { - "x": 1593413302000, - "y": 5624, - }, - Object { - "x": 1593413303000, - "y": 11360, - }, - Object { - "x": 1593413304000, - "y": 12320, - }, - Object { - "x": 1593413305000, - "y": 38640, - }, - Object { - "x": 1593413306000, - "y": 9728, - }, - Object { - "x": 1593413307000, - "y": 17016, - }, - Object { - "x": 1593413308000, - "y": 26848, - }, - Object { - "x": 1593413309000, - "y": 1753072, - }, - Object { - "x": 1593413310000, - "y": 16992, - }, - Object { - "x": 1593413311000, - "y": 26560, - }, - Object { - "x": 1593413312000, - "y": 11232, - }, - Object { - "x": 1593413313000, - "y": 11424, - }, - Object { - "x": 1593413314000, - "y": 16096, - }, - Object { - "x": 1593413315000, - "y": 18800, - }, - Object { - "x": 1593413316000, - "y": 12672, - }, - Object { - "x": 1593413317000, - "y": 24316, - }, - Object { - "x": 1593413318000, - "y": 8944, - }, - Object { - "x": 1593413319000, - "y": 272352, - }, - Object { - "x": 1593413320000, - "y": 7992, - }, - Object { - "x": 1593413321000, - "y": 8368, - }, - Object { - "x": 1593413322000, - "y": 1928, - }, - Object { - "x": 1593413323000, - "y": null, - }, - Object { - "x": 1593413324000, - "y": null, - }, - Object { - "x": 1593413325000, - "y": null, - }, - Object { - "x": 1593413326000, - "y": null, - }, - Object { - "x": 1593413327000, - "y": null, - }, - Object { - "x": 1593413328000, - "y": null, - }, - Object { - "x": 1593413329000, - "y": null, - }, - Object { - "x": 1593413330000, - "y": null, - }, - Object { - "x": 1593413331000, - "y": null, - }, - Object { - "x": 1593413332000, - "y": null, - }, - Object { - "x": 1593413333000, - "y": null, - }, - Object { - "x": 1593413334000, - "y": null, - }, - Object { - "x": 1593413335000, - "y": null, - }, - Object { - "x": 1593413336000, - "y": null, - }, - Object { - "x": 1593413337000, - "y": null, - }, - Object { - "x": 1593413338000, - "y": null, - }, - Object { - "x": 1593413339000, - "y": null, - }, - Object { - "x": 1593413340000, - "y": null, - }, - ], - }, - "tpmBuckets": Array [ - Object { - "avg": 24.75, - "dataPoints": Array [ - Object { - "x": 1593413100000, - "y": 0, - }, - Object { - "x": 1593413101000, - "y": 0, - }, - Object { - "x": 1593413102000, - "y": 0, - }, - Object { - "x": 1593413103000, - "y": 0, - }, - Object { - "x": 1593413104000, - "y": 0, - }, - Object { - "x": 1593413105000, - "y": 0, - }, - Object { - "x": 1593413106000, - "y": 0, - }, - Object { - "x": 1593413107000, - "y": 0, - }, - Object { - "x": 1593413108000, - "y": 0, - }, - Object { - "x": 1593413109000, - "y": 0, - }, - Object { - "x": 1593413110000, - "y": 0, - }, - Object { - "x": 1593413111000, - "y": 0, - }, - Object { - "x": 1593413112000, - "y": 0, - }, - Object { - "x": 1593413113000, - "y": 0, - }, - Object { - "x": 1593413114000, - "y": 0, - }, - Object { - "x": 1593413115000, - "y": 0, - }, - Object { - "x": 1593413116000, - "y": 0, - }, - Object { - "x": 1593413117000, - "y": 0, - }, - Object { - "x": 1593413118000, - "y": 0, - }, - Object { - "x": 1593413119000, - "y": 0, - }, - Object { - "x": 1593413120000, - "y": 0, - }, - Object { - "x": 1593413121000, - "y": 0, - }, - Object { - "x": 1593413122000, - "y": 0, - }, - Object { - "x": 1593413123000, - "y": 0, - }, - Object { - "x": 1593413124000, - "y": 0, - }, - Object { - "x": 1593413125000, - "y": 0, - }, - Object { - "x": 1593413126000, - "y": 0, - }, - Object { - "x": 1593413127000, - "y": 0, - }, - Object { - "x": 1593413128000, - "y": 0, - }, - Object { - "x": 1593413129000, - "y": 0, - }, - Object { - "x": 1593413130000, - "y": 0, - }, - Object { - "x": 1593413131000, - "y": 0, - }, - Object { - "x": 1593413132000, - "y": 0, - }, - Object { - "x": 1593413133000, - "y": 0, - }, - Object { - "x": 1593413134000, - "y": 0, - }, - Object { - "x": 1593413135000, - "y": 0, - }, - Object { - "x": 1593413136000, - "y": 0, - }, - Object { - "x": 1593413137000, - "y": 0, - }, - Object { - "x": 1593413138000, - "y": 0, - }, - Object { - "x": 1593413139000, - "y": 0, - }, - Object { - "x": 1593413140000, - "y": 0, - }, - Object { - "x": 1593413141000, - "y": 0, - }, - Object { - "x": 1593413142000, - "y": 0, - }, - Object { - "x": 1593413143000, - "y": 0, - }, - Object { - "x": 1593413144000, - "y": 0, - }, - Object { - "x": 1593413145000, - "y": 0, - }, - Object { - "x": 1593413146000, - "y": 0, - }, - Object { - "x": 1593413147000, - "y": 0, - }, - Object { - "x": 1593413148000, - "y": 0, - }, - Object { - "x": 1593413149000, - "y": 0, - }, - Object { - "x": 1593413150000, - "y": 0, - }, - Object { - "x": 1593413151000, - "y": 0, - }, - Object { - "x": 1593413152000, - "y": 0, - }, - Object { - "x": 1593413153000, - "y": 0, - }, - Object { - "x": 1593413154000, - "y": 0, - }, - Object { - "x": 1593413155000, - "y": 0, - }, - Object { - "x": 1593413156000, - "y": 0, - }, - Object { - "x": 1593413157000, - "y": 0, - }, - Object { - "x": 1593413158000, - "y": 0, - }, - Object { - "x": 1593413159000, - "y": 0, - }, - Object { - "x": 1593413160000, - "y": 0, - }, - Object { - "x": 1593413161000, - "y": 0, - }, - Object { - "x": 1593413162000, - "y": 0, - }, - Object { - "x": 1593413163000, - "y": 0, - }, - Object { - "x": 1593413164000, - "y": 0, - }, - Object { - "x": 1593413165000, - "y": 0, - }, - Object { - "x": 1593413166000, - "y": 0, - }, - Object { - "x": 1593413167000, - "y": 0, - }, - Object { - "x": 1593413168000, - "y": 0, - }, - Object { - "x": 1593413169000, - "y": 0, - }, - Object { - "x": 1593413170000, - "y": 0, - }, - Object { - "x": 1593413171000, - "y": 0, - }, - Object { - "x": 1593413172000, - "y": 0, - }, - Object { - "x": 1593413173000, - "y": 0, - }, - Object { - "x": 1593413174000, - "y": 0, - }, - Object { - "x": 1593413175000, - "y": 0, - }, - Object { - "x": 1593413176000, - "y": 0, - }, - Object { - "x": 1593413177000, - "y": 0, - }, - Object { - "x": 1593413178000, - "y": 0, - }, - Object { - "x": 1593413179000, - "y": 0, - }, - Object { - "x": 1593413180000, - "y": 0, - }, - Object { - "x": 1593413181000, - "y": 0, - }, - Object { - "x": 1593413182000, - "y": 0, - }, - Object { - "x": 1593413183000, - "y": 0, - }, - Object { - "x": 1593413184000, - "y": 0, - }, - Object { - "x": 1593413185000, - "y": 0, - }, - Object { - "x": 1593413186000, - "y": 0, - }, - Object { - "x": 1593413187000, - "y": 0, - }, - Object { - "x": 1593413188000, - "y": 0, - }, - Object { - "x": 1593413189000, - "y": 0, - }, - Object { - "x": 1593413190000, - "y": 0, - }, - Object { - "x": 1593413191000, - "y": 0, - }, - Object { - "x": 1593413192000, - "y": 0, - }, - Object { - "x": 1593413193000, - "y": 0, - }, - Object { - "x": 1593413194000, - "y": 0, - }, - Object { - "x": 1593413195000, - "y": 0, - }, - Object { - "x": 1593413196000, - "y": 0, - }, - Object { - "x": 1593413197000, - "y": 0, - }, - Object { - "x": 1593413198000, - "y": 0, - }, - Object { - "x": 1593413199000, - "y": 0, - }, - Object { - "x": 1593413200000, - "y": 0, - }, - Object { - "x": 1593413201000, - "y": 0, - }, - Object { - "x": 1593413202000, - "y": 0, - }, - Object { - "x": 1593413203000, - "y": 0, - }, - Object { - "x": 1593413204000, - "y": 0, - }, - Object { - "x": 1593413205000, - "y": 0, - }, - Object { - "x": 1593413206000, - "y": 0, - }, - Object { - "x": 1593413207000, - "y": 0, - }, - Object { - "x": 1593413208000, - "y": 0, - }, - Object { - "x": 1593413209000, - "y": 0, - }, - Object { - "x": 1593413210000, - "y": 0, - }, - Object { - "x": 1593413211000, - "y": 0, - }, - Object { - "x": 1593413212000, - "y": 0, - }, - Object { - "x": 1593413213000, - "y": 0, - }, - Object { - "x": 1593413214000, - "y": 0, - }, - Object { - "x": 1593413215000, - "y": 0, - }, - Object { - "x": 1593413216000, - "y": 0, - }, - Object { - "x": 1593413217000, - "y": 0, - }, - Object { - "x": 1593413218000, - "y": 0, - }, - Object { - "x": 1593413219000, - "y": 0, - }, - Object { - "x": 1593413220000, - "y": 0, - }, - Object { - "x": 1593413221000, - "y": 0, - }, - Object { - "x": 1593413222000, - "y": 0, - }, - Object { - "x": 1593413223000, - "y": 0, - }, - Object { - "x": 1593413224000, - "y": 0, - }, - Object { - "x": 1593413225000, - "y": 0, - }, - Object { - "x": 1593413226000, - "y": 0, - }, - Object { - "x": 1593413227000, - "y": 0, - }, - Object { - "x": 1593413228000, - "y": 0, - }, - Object { - "x": 1593413229000, - "y": 0, - }, - Object { - "x": 1593413230000, - "y": 0, - }, - Object { - "x": 1593413231000, - "y": 0, - }, - Object { - "x": 1593413232000, - "y": 0, - }, - Object { - "x": 1593413233000, - "y": 0, - }, - Object { - "x": 1593413234000, - "y": 0, - }, - Object { - "x": 1593413235000, - "y": 0, - }, - Object { - "x": 1593413236000, - "y": 0, - }, - Object { - "x": 1593413237000, - "y": 0, - }, - Object { - "x": 1593413238000, - "y": 0, - }, - Object { - "x": 1593413239000, - "y": 0, - }, - Object { - "x": 1593413240000, - "y": 0, - }, - Object { - "x": 1593413241000, - "y": 0, - }, - Object { - "x": 1593413242000, - "y": 0, - }, - Object { - "x": 1593413243000, - "y": 0, - }, - Object { - "x": 1593413244000, - "y": 0, - }, - Object { - "x": 1593413245000, - "y": 0, - }, - Object { - "x": 1593413246000, - "y": 0, - }, - Object { - "x": 1593413247000, - "y": 0, - }, - Object { - "x": 1593413248000, - "y": 0, - }, - Object { - "x": 1593413249000, - "y": 0, - }, - Object { - "x": 1593413250000, - "y": 0, - }, - Object { - "x": 1593413251000, - "y": 0, - }, - Object { - "x": 1593413252000, - "y": 0, - }, - Object { - "x": 1593413253000, - "y": 0, - }, - Object { - "x": 1593413254000, - "y": 0, - }, - Object { - "x": 1593413255000, - "y": 0, - }, - Object { - "x": 1593413256000, - "y": 0, - }, - Object { - "x": 1593413257000, - "y": 0, - }, - Object { - "x": 1593413258000, - "y": 0, - }, - Object { - "x": 1593413259000, - "y": 0, - }, - Object { - "x": 1593413260000, - "y": 0, - }, - Object { - "x": 1593413261000, - "y": 0, - }, - Object { - "x": 1593413262000, - "y": 0, - }, - Object { - "x": 1593413263000, - "y": 0, - }, - Object { - "x": 1593413264000, - "y": 0, - }, - Object { - "x": 1593413265000, - "y": 0, - }, - Object { - "x": 1593413266000, - "y": 0, - }, - Object { - "x": 1593413267000, - "y": 0, - }, - Object { - "x": 1593413268000, - "y": 0, - }, - Object { - "x": 1593413269000, - "y": 0, - }, - Object { - "x": 1593413270000, - "y": 0, - }, - Object { - "x": 1593413271000, - "y": 0, - }, - Object { - "x": 1593413272000, - "y": 1, - }, - Object { - "x": 1593413273000, - "y": 2, - }, - Object { - "x": 1593413274000, - "y": 0, - }, - Object { - "x": 1593413275000, - "y": 0, - }, - Object { - "x": 1593413276000, - "y": 0, - }, - Object { - "x": 1593413277000, - "y": 1, - }, - Object { - "x": 1593413278000, - "y": 0, - }, - Object { - "x": 1593413279000, - "y": 0, - }, - Object { - "x": 1593413280000, - "y": 0, - }, - Object { - "x": 1593413281000, - "y": 1, - }, - Object { - "x": 1593413282000, - "y": 0, - }, - Object { - "x": 1593413283000, - "y": 0, - }, - Object { - "x": 1593413284000, - "y": 2, - }, - Object { - "x": 1593413285000, - "y": 2, - }, - Object { - "x": 1593413286000, - "y": 7, - }, - Object { - "x": 1593413287000, - "y": 1, - }, - Object { - "x": 1593413288000, - "y": 2, - }, - Object { - "x": 1593413289000, - "y": 1, - }, - Object { - "x": 1593413290000, - "y": 4, - }, - Object { - "x": 1593413291000, - "y": 2, - }, - Object { - "x": 1593413292000, - "y": 1, - }, - Object { - "x": 1593413293000, - "y": 2, - }, - Object { - "x": 1593413294000, - "y": 3, - }, - Object { - "x": 1593413295000, - "y": 2, - }, - Object { - "x": 1593413296000, - "y": 2, - }, - Object { - "x": 1593413297000, - "y": 2, - }, - Object { - "x": 1593413298000, - "y": 6, - }, - Object { - "x": 1593413299000, - "y": 1, - }, - Object { - "x": 1593413300000, - "y": 2, - }, - Object { - "x": 1593413301000, - "y": 3, - }, - Object { - "x": 1593413302000, - "y": 2, - }, - Object { - "x": 1593413303000, - "y": 2, - }, - Object { - "x": 1593413304000, - "y": 2, - }, - Object { - "x": 1593413305000, - "y": 1, - }, - Object { - "x": 1593413306000, - "y": 2, - }, - Object { - "x": 1593413307000, - "y": 3, - }, - Object { - "x": 1593413308000, - "y": 2, - }, - Object { - "x": 1593413309000, - "y": 2, - }, - Object { - "x": 1593413310000, - "y": 2, - }, - Object { - "x": 1593413311000, - "y": 1, - }, - Object { - "x": 1593413312000, - "y": 3, - }, - Object { - "x": 1593413313000, - "y": 3, - }, - Object { - "x": 1593413314000, - "y": 5, - }, - Object { - "x": 1593413315000, - "y": 2, - }, - Object { - "x": 1593413316000, - "y": 2, - }, - Object { - "x": 1593413317000, - "y": 6, - }, - Object { - "x": 1593413318000, - "y": 2, - }, - Object { - "x": 1593413319000, - "y": 2, - }, - Object { - "x": 1593413320000, - "y": 2, - }, - Object { - "x": 1593413321000, - "y": 2, - }, - Object { - "x": 1593413322000, - "y": 1, - }, - Object { - "x": 1593413323000, - "y": 0, - }, - Object { - "x": 1593413324000, - "y": 0, - }, - Object { - "x": 1593413325000, - "y": 0, - }, - Object { - "x": 1593413326000, - "y": 0, - }, - Object { - "x": 1593413327000, - "y": 0, - }, - Object { - "x": 1593413328000, - "y": 0, - }, - Object { - "x": 1593413329000, - "y": 0, - }, - Object { - "x": 1593413330000, - "y": 0, - }, - Object { - "x": 1593413331000, - "y": 0, - }, - Object { - "x": 1593413332000, - "y": 0, - }, - Object { - "x": 1593413333000, - "y": 0, - }, - Object { - "x": 1593413334000, - "y": 0, - }, - Object { - "x": 1593413335000, - "y": 0, - }, - Object { - "x": 1593413336000, - "y": 0, - }, - Object { - "x": 1593413337000, - "y": 0, - }, - Object { - "x": 1593413338000, - "y": 0, - }, - Object { - "x": 1593413339000, - "y": 0, - }, - Object { - "x": 1593413340000, - "y": 0, - }, - ], - "key": "HTTP 2xx", - }, - Object { - "avg": 1.75, - "dataPoints": Array [ - Object { - "x": 1593413100000, - "y": 0, - }, - Object { - "x": 1593413101000, - "y": 0, - }, - Object { - "x": 1593413102000, - "y": 0, - }, - Object { - "x": 1593413103000, - "y": 0, - }, - Object { - "x": 1593413104000, - "y": 0, - }, - Object { - "x": 1593413105000, - "y": 0, - }, - Object { - "x": 1593413106000, - "y": 0, - }, - Object { - "x": 1593413107000, - "y": 0, - }, - Object { - "x": 1593413108000, - "y": 0, - }, - Object { - "x": 1593413109000, - "y": 0, - }, - Object { - "x": 1593413110000, - "y": 0, - }, - Object { - "x": 1593413111000, - "y": 0, - }, - Object { - "x": 1593413112000, - "y": 0, - }, - Object { - "x": 1593413113000, - "y": 0, - }, - Object { - "x": 1593413114000, - "y": 0, - }, - Object { - "x": 1593413115000, - "y": 0, - }, - Object { - "x": 1593413116000, - "y": 0, - }, - Object { - "x": 1593413117000, - "y": 0, - }, - Object { - "x": 1593413118000, - "y": 0, - }, - Object { - "x": 1593413119000, - "y": 0, - }, - Object { - "x": 1593413120000, - "y": 0, - }, - Object { - "x": 1593413121000, - "y": 0, - }, - Object { - "x": 1593413122000, - "y": 0, - }, - Object { - "x": 1593413123000, - "y": 0, - }, - Object { - "x": 1593413124000, - "y": 0, - }, - Object { - "x": 1593413125000, - "y": 0, - }, - Object { - "x": 1593413126000, - "y": 0, - }, - Object { - "x": 1593413127000, - "y": 0, - }, - Object { - "x": 1593413128000, - "y": 0, - }, - Object { - "x": 1593413129000, - "y": 0, - }, - Object { - "x": 1593413130000, - "y": 0, - }, - Object { - "x": 1593413131000, - "y": 0, - }, - Object { - "x": 1593413132000, - "y": 0, - }, - Object { - "x": 1593413133000, - "y": 0, - }, - Object { - "x": 1593413134000, - "y": 0, - }, - Object { - "x": 1593413135000, - "y": 0, - }, - Object { - "x": 1593413136000, - "y": 0, - }, - Object { - "x": 1593413137000, - "y": 0, - }, - Object { - "x": 1593413138000, - "y": 0, - }, - Object { - "x": 1593413139000, - "y": 0, - }, - Object { - "x": 1593413140000, - "y": 0, - }, - Object { - "x": 1593413141000, - "y": 0, - }, - Object { - "x": 1593413142000, - "y": 0, - }, - Object { - "x": 1593413143000, - "y": 0, - }, - Object { - "x": 1593413144000, - "y": 0, - }, - Object { - "x": 1593413145000, - "y": 0, - }, - Object { - "x": 1593413146000, - "y": 0, - }, - Object { - "x": 1593413147000, - "y": 0, - }, - Object { - "x": 1593413148000, - "y": 0, - }, - Object { - "x": 1593413149000, - "y": 0, - }, - Object { - "x": 1593413150000, - "y": 0, - }, - Object { - "x": 1593413151000, - "y": 0, - }, - Object { - "x": 1593413152000, - "y": 0, - }, - Object { - "x": 1593413153000, - "y": 0, - }, - Object { - "x": 1593413154000, - "y": 0, - }, - Object { - "x": 1593413155000, - "y": 0, - }, - Object { - "x": 1593413156000, - "y": 0, - }, - Object { - "x": 1593413157000, - "y": 0, - }, - Object { - "x": 1593413158000, - "y": 0, - }, - Object { - "x": 1593413159000, - "y": 0, - }, - Object { - "x": 1593413160000, - "y": 0, - }, - Object { - "x": 1593413161000, - "y": 0, - }, - Object { - "x": 1593413162000, - "y": 0, - }, - Object { - "x": 1593413163000, - "y": 0, - }, - Object { - "x": 1593413164000, - "y": 0, - }, - Object { - "x": 1593413165000, - "y": 0, - }, - Object { - "x": 1593413166000, - "y": 0, - }, - Object { - "x": 1593413167000, - "y": 0, - }, - Object { - "x": 1593413168000, - "y": 0, - }, - Object { - "x": 1593413169000, - "y": 0, - }, - Object { - "x": 1593413170000, - "y": 0, - }, - Object { - "x": 1593413171000, - "y": 0, - }, - Object { - "x": 1593413172000, - "y": 0, - }, - Object { - "x": 1593413173000, - "y": 0, - }, - Object { - "x": 1593413174000, - "y": 0, - }, - Object { - "x": 1593413175000, - "y": 0, - }, - Object { - "x": 1593413176000, - "y": 0, - }, - Object { - "x": 1593413177000, - "y": 0, - }, - Object { - "x": 1593413178000, - "y": 0, - }, - Object { - "x": 1593413179000, - "y": 0, - }, - Object { - "x": 1593413180000, - "y": 0, - }, - Object { - "x": 1593413181000, - "y": 0, - }, - Object { - "x": 1593413182000, - "y": 0, - }, - Object { - "x": 1593413183000, - "y": 0, - }, - Object { - "x": 1593413184000, - "y": 0, - }, - Object { - "x": 1593413185000, - "y": 0, - }, - Object { - "x": 1593413186000, - "y": 0, - }, - Object { - "x": 1593413187000, - "y": 0, - }, - Object { - "x": 1593413188000, - "y": 0, - }, - Object { - "x": 1593413189000, - "y": 0, - }, - Object { - "x": 1593413190000, - "y": 0, - }, - Object { - "x": 1593413191000, - "y": 0, - }, - Object { - "x": 1593413192000, - "y": 0, - }, - Object { - "x": 1593413193000, - "y": 0, - }, - Object { - "x": 1593413194000, - "y": 0, - }, - Object { - "x": 1593413195000, - "y": 0, - }, - Object { - "x": 1593413196000, - "y": 0, - }, - Object { - "x": 1593413197000, - "y": 0, - }, - Object { - "x": 1593413198000, - "y": 0, - }, - Object { - "x": 1593413199000, - "y": 0, - }, - Object { - "x": 1593413200000, - "y": 0, - }, - Object { - "x": 1593413201000, - "y": 0, - }, - Object { - "x": 1593413202000, - "y": 0, - }, - Object { - "x": 1593413203000, - "y": 0, - }, - Object { - "x": 1593413204000, - "y": 0, - }, - Object { - "x": 1593413205000, - "y": 0, - }, - Object { - "x": 1593413206000, - "y": 0, - }, - Object { - "x": 1593413207000, - "y": 0, - }, - Object { - "x": 1593413208000, - "y": 0, - }, - Object { - "x": 1593413209000, - "y": 0, - }, - Object { - "x": 1593413210000, - "y": 0, - }, - Object { - "x": 1593413211000, - "y": 0, - }, - Object { - "x": 1593413212000, - "y": 0, - }, - Object { - "x": 1593413213000, - "y": 0, - }, - Object { - "x": 1593413214000, - "y": 0, - }, - Object { - "x": 1593413215000, - "y": 0, - }, - Object { - "x": 1593413216000, - "y": 0, - }, - Object { - "x": 1593413217000, - "y": 0, - }, - Object { - "x": 1593413218000, - "y": 0, - }, - Object { - "x": 1593413219000, - "y": 0, - }, - Object { - "x": 1593413220000, - "y": 0, - }, - Object { - "x": 1593413221000, - "y": 0, - }, - Object { - "x": 1593413222000, - "y": 0, - }, - Object { - "x": 1593413223000, - "y": 0, - }, - Object { - "x": 1593413224000, - "y": 0, - }, - Object { - "x": 1593413225000, - "y": 0, - }, - Object { - "x": 1593413226000, - "y": 0, - }, - Object { - "x": 1593413227000, - "y": 0, - }, - Object { - "x": 1593413228000, - "y": 0, - }, - Object { - "x": 1593413229000, - "y": 0, - }, - Object { - "x": 1593413230000, - "y": 0, - }, - Object { - "x": 1593413231000, - "y": 0, - }, - Object { - "x": 1593413232000, - "y": 0, - }, - Object { - "x": 1593413233000, - "y": 0, - }, - Object { - "x": 1593413234000, - "y": 0, - }, - Object { - "x": 1593413235000, - "y": 0, - }, - Object { - "x": 1593413236000, - "y": 0, - }, - Object { - "x": 1593413237000, - "y": 0, - }, - Object { - "x": 1593413238000, - "y": 0, - }, - Object { - "x": 1593413239000, - "y": 0, - }, - Object { - "x": 1593413240000, - "y": 0, - }, - Object { - "x": 1593413241000, - "y": 0, - }, - Object { - "x": 1593413242000, - "y": 0, - }, - Object { - "x": 1593413243000, - "y": 0, - }, - Object { - "x": 1593413244000, - "y": 0, - }, - Object { - "x": 1593413245000, - "y": 0, - }, - Object { - "x": 1593413246000, - "y": 0, - }, - Object { - "x": 1593413247000, - "y": 0, - }, - Object { - "x": 1593413248000, - "y": 0, - }, - Object { - "x": 1593413249000, - "y": 0, - }, - Object { - "x": 1593413250000, - "y": 0, - }, - Object { - "x": 1593413251000, - "y": 0, - }, - Object { - "x": 1593413252000, - "y": 0, - }, - Object { - "x": 1593413253000, - "y": 0, - }, - Object { - "x": 1593413254000, - "y": 0, - }, - Object { - "x": 1593413255000, - "y": 0, - }, - Object { - "x": 1593413256000, - "y": 0, - }, - Object { - "x": 1593413257000, - "y": 0, - }, - Object { - "x": 1593413258000, - "y": 0, - }, - Object { - "x": 1593413259000, - "y": 0, - }, - Object { - "x": 1593413260000, - "y": 0, - }, - Object { - "x": 1593413261000, - "y": 0, - }, - Object { - "x": 1593413262000, - "y": 0, - }, - Object { - "x": 1593413263000, - "y": 0, - }, - Object { - "x": 1593413264000, - "y": 0, - }, - Object { - "x": 1593413265000, - "y": 0, - }, - Object { - "x": 1593413266000, - "y": 0, - }, - Object { - "x": 1593413267000, - "y": 0, - }, - Object { - "x": 1593413268000, - "y": 0, - }, - Object { - "x": 1593413269000, - "y": 0, - }, - Object { - "x": 1593413270000, - "y": 0, - }, - Object { - "x": 1593413271000, - "y": 0, - }, - Object { - "x": 1593413272000, - "y": 0, - }, - Object { - "x": 1593413273000, - "y": 0, - }, - Object { - "x": 1593413274000, - "y": 0, - }, - Object { - "x": 1593413275000, - "y": 0, - }, - Object { - "x": 1593413276000, - "y": 0, - }, - Object { - "x": 1593413277000, - "y": 0, - }, - Object { - "x": 1593413278000, - "y": 0, - }, - Object { - "x": 1593413279000, - "y": 0, - }, - Object { - "x": 1593413280000, - "y": 0, - }, - Object { - "x": 1593413281000, - "y": 0, - }, - Object { - "x": 1593413282000, - "y": 0, - }, - Object { - "x": 1593413283000, - "y": 0, - }, - Object { - "x": 1593413284000, - "y": 0, - }, - Object { - "x": 1593413285000, - "y": 0, - }, - Object { - "x": 1593413286000, - "y": 0, - }, - Object { - "x": 1593413287000, - "y": 0, - }, - Object { - "x": 1593413288000, - "y": 0, - }, - Object { - "x": 1593413289000, - "y": 0, - }, - Object { - "x": 1593413290000, - "y": 0, - }, - Object { - "x": 1593413291000, - "y": 0, - }, - Object { - "x": 1593413292000, - "y": 0, - }, - Object { - "x": 1593413293000, - "y": 0, - }, - Object { - "x": 1593413294000, - "y": 0, - }, - Object { - "x": 1593413295000, - "y": 0, - }, - Object { - "x": 1593413296000, - "y": 0, - }, - Object { - "x": 1593413297000, - "y": 0, - }, - Object { - "x": 1593413298000, - "y": 2, - }, - Object { - "x": 1593413299000, - "y": 0, - }, - Object { - "x": 1593413300000, - "y": 0, - }, - Object { - "x": 1593413301000, - "y": 3, - }, - Object { - "x": 1593413302000, - "y": 0, - }, - Object { - "x": 1593413303000, - "y": 0, - }, - Object { - "x": 1593413304000, - "y": 0, - }, - Object { - "x": 1593413305000, - "y": 0, - }, - Object { - "x": 1593413306000, - "y": 0, - }, - Object { - "x": 1593413307000, - "y": 0, - }, - Object { - "x": 1593413308000, - "y": 0, - }, - Object { - "x": 1593413309000, - "y": 0, - }, - Object { - "x": 1593413310000, - "y": 0, - }, - Object { - "x": 1593413311000, - "y": 0, - }, - Object { - "x": 1593413312000, - "y": 0, - }, - Object { - "x": 1593413313000, - "y": 0, - }, - Object { - "x": 1593413314000, - "y": 0, - }, - Object { - "x": 1593413315000, - "y": 0, - }, - Object { - "x": 1593413316000, - "y": 0, - }, - Object { - "x": 1593413317000, - "y": 2, - }, - Object { - "x": 1593413318000, - "y": 0, - }, - Object { - "x": 1593413319000, - "y": 0, - }, - Object { - "x": 1593413320000, - "y": 0, - }, - Object { - "x": 1593413321000, - "y": 0, - }, - Object { - "x": 1593413322000, - "y": 0, - }, - Object { - "x": 1593413323000, - "y": 0, - }, - Object { - "x": 1593413324000, - "y": 0, - }, - Object { - "x": 1593413325000, - "y": 0, - }, - Object { - "x": 1593413326000, - "y": 0, - }, - Object { - "x": 1593413327000, - "y": 0, - }, - Object { - "x": 1593413328000, - "y": 0, - }, - Object { - "x": 1593413329000, - "y": 0, - }, - Object { - "x": 1593413330000, - "y": 0, - }, - Object { - "x": 1593413331000, - "y": 0, - }, - Object { - "x": 1593413332000, - "y": 0, - }, - Object { - "x": 1593413333000, - "y": 0, - }, - Object { - "x": 1593413334000, - "y": 0, - }, - Object { - "x": 1593413335000, - "y": 0, - }, - Object { - "x": 1593413336000, - "y": 0, - }, - Object { - "x": 1593413337000, - "y": 0, - }, - Object { - "x": 1593413338000, - "y": 0, - }, - Object { - "x": 1593413339000, - "y": 0, - }, - Object { - "x": 1593413340000, - "y": 0, - }, - ], - "key": "HTTP 3xx", - }, - Object { - "avg": 2, - "dataPoints": Array [ - Object { - "x": 1593413100000, - "y": 0, - }, - Object { - "x": 1593413101000, - "y": 0, - }, - Object { - "x": 1593413102000, - "y": 0, - }, - Object { - "x": 1593413103000, - "y": 0, - }, - Object { - "x": 1593413104000, - "y": 0, - }, - Object { - "x": 1593413105000, - "y": 0, - }, - Object { - "x": 1593413106000, - "y": 0, - }, - Object { - "x": 1593413107000, - "y": 0, - }, - Object { - "x": 1593413108000, - "y": 0, - }, - Object { - "x": 1593413109000, - "y": 0, - }, - Object { - "x": 1593413110000, - "y": 0, - }, - Object { - "x": 1593413111000, - "y": 0, - }, - Object { - "x": 1593413112000, - "y": 0, - }, - Object { - "x": 1593413113000, - "y": 0, - }, - Object { - "x": 1593413114000, - "y": 0, - }, - Object { - "x": 1593413115000, - "y": 0, - }, - Object { - "x": 1593413116000, - "y": 0, - }, - Object { - "x": 1593413117000, - "y": 0, - }, - Object { - "x": 1593413118000, - "y": 0, - }, - Object { - "x": 1593413119000, - "y": 0, - }, - Object { - "x": 1593413120000, - "y": 0, - }, - Object { - "x": 1593413121000, - "y": 0, - }, - Object { - "x": 1593413122000, - "y": 0, - }, - Object { - "x": 1593413123000, - "y": 0, - }, - Object { - "x": 1593413124000, - "y": 0, - }, - Object { - "x": 1593413125000, - "y": 0, - }, - Object { - "x": 1593413126000, - "y": 0, - }, - Object { - "x": 1593413127000, - "y": 0, - }, - Object { - "x": 1593413128000, - "y": 0, - }, - Object { - "x": 1593413129000, - "y": 0, - }, - Object { - "x": 1593413130000, - "y": 0, - }, - Object { - "x": 1593413131000, - "y": 0, - }, - Object { - "x": 1593413132000, - "y": 0, - }, - Object { - "x": 1593413133000, - "y": 0, - }, - Object { - "x": 1593413134000, - "y": 0, - }, - Object { - "x": 1593413135000, - "y": 0, - }, - Object { - "x": 1593413136000, - "y": 0, - }, - Object { - "x": 1593413137000, - "y": 0, - }, - Object { - "x": 1593413138000, - "y": 0, - }, - Object { - "x": 1593413139000, - "y": 0, - }, - Object { - "x": 1593413140000, - "y": 0, - }, - Object { - "x": 1593413141000, - "y": 0, - }, - Object { - "x": 1593413142000, - "y": 0, - }, - Object { - "x": 1593413143000, - "y": 0, - }, - Object { - "x": 1593413144000, - "y": 0, - }, - Object { - "x": 1593413145000, - "y": 0, - }, - Object { - "x": 1593413146000, - "y": 0, - }, - Object { - "x": 1593413147000, - "y": 0, - }, - Object { - "x": 1593413148000, - "y": 0, - }, - Object { - "x": 1593413149000, - "y": 0, - }, - Object { - "x": 1593413150000, - "y": 0, - }, - Object { - "x": 1593413151000, - "y": 0, - }, - Object { - "x": 1593413152000, - "y": 0, - }, - Object { - "x": 1593413153000, - "y": 0, - }, - Object { - "x": 1593413154000, - "y": 0, - }, - Object { - "x": 1593413155000, - "y": 0, - }, - Object { - "x": 1593413156000, - "y": 0, - }, - Object { - "x": 1593413157000, - "y": 0, - }, - Object { - "x": 1593413158000, - "y": 0, - }, - Object { - "x": 1593413159000, - "y": 0, - }, - Object { - "x": 1593413160000, - "y": 0, - }, - Object { - "x": 1593413161000, - "y": 0, - }, - Object { - "x": 1593413162000, - "y": 0, - }, - Object { - "x": 1593413163000, - "y": 0, - }, - Object { - "x": 1593413164000, - "y": 0, - }, - Object { - "x": 1593413165000, - "y": 0, - }, - Object { - "x": 1593413166000, - "y": 0, - }, - Object { - "x": 1593413167000, - "y": 0, - }, - Object { - "x": 1593413168000, - "y": 0, - }, - Object { - "x": 1593413169000, - "y": 0, - }, - Object { - "x": 1593413170000, - "y": 0, - }, - Object { - "x": 1593413171000, - "y": 0, - }, - Object { - "x": 1593413172000, - "y": 0, - }, - Object { - "x": 1593413173000, - "y": 0, - }, - Object { - "x": 1593413174000, - "y": 0, - }, - Object { - "x": 1593413175000, - "y": 0, - }, - Object { - "x": 1593413176000, - "y": 0, - }, - Object { - "x": 1593413177000, - "y": 0, - }, - Object { - "x": 1593413178000, - "y": 0, - }, - Object { - "x": 1593413179000, - "y": 0, - }, - Object { - "x": 1593413180000, - "y": 0, - }, - Object { - "x": 1593413181000, - "y": 0, - }, - Object { - "x": 1593413182000, - "y": 0, - }, - Object { - "x": 1593413183000, - "y": 0, - }, - Object { - "x": 1593413184000, - "y": 0, - }, - Object { - "x": 1593413185000, - "y": 0, - }, - Object { - "x": 1593413186000, - "y": 0, - }, - Object { - "x": 1593413187000, - "y": 0, - }, - Object { - "x": 1593413188000, - "y": 0, - }, - Object { - "x": 1593413189000, - "y": 0, - }, - Object { - "x": 1593413190000, - "y": 0, - }, - Object { - "x": 1593413191000, - "y": 0, - }, - Object { - "x": 1593413192000, - "y": 0, - }, - Object { - "x": 1593413193000, - "y": 0, - }, - Object { - "x": 1593413194000, - "y": 0, - }, - Object { - "x": 1593413195000, - "y": 0, - }, - Object { - "x": 1593413196000, - "y": 0, - }, - Object { - "x": 1593413197000, - "y": 0, - }, - Object { - "x": 1593413198000, - "y": 0, - }, - Object { - "x": 1593413199000, - "y": 0, - }, - Object { - "x": 1593413200000, - "y": 0, - }, - Object { - "x": 1593413201000, - "y": 0, - }, - Object { - "x": 1593413202000, - "y": 0, - }, - Object { - "x": 1593413203000, - "y": 0, - }, - Object { - "x": 1593413204000, - "y": 0, - }, - Object { - "x": 1593413205000, - "y": 0, - }, - Object { - "x": 1593413206000, - "y": 0, - }, - Object { - "x": 1593413207000, - "y": 0, - }, - Object { - "x": 1593413208000, - "y": 0, - }, - Object { - "x": 1593413209000, - "y": 0, - }, - Object { - "x": 1593413210000, - "y": 0, - }, - Object { - "x": 1593413211000, - "y": 0, - }, - Object { - "x": 1593413212000, - "y": 0, - }, - Object { - "x": 1593413213000, - "y": 0, - }, - Object { - "x": 1593413214000, - "y": 0, - }, - Object { - "x": 1593413215000, - "y": 0, - }, - Object { - "x": 1593413216000, - "y": 0, - }, - Object { - "x": 1593413217000, - "y": 0, - }, - Object { - "x": 1593413218000, - "y": 0, - }, - Object { - "x": 1593413219000, - "y": 0, - }, - Object { - "x": 1593413220000, - "y": 0, - }, - Object { - "x": 1593413221000, - "y": 0, - }, - Object { - "x": 1593413222000, - "y": 0, - }, - Object { - "x": 1593413223000, - "y": 0, - }, - Object { - "x": 1593413224000, - "y": 0, - }, - Object { - "x": 1593413225000, - "y": 0, - }, - Object { - "x": 1593413226000, - "y": 0, - }, - Object { - "x": 1593413227000, - "y": 0, - }, - Object { - "x": 1593413228000, - "y": 0, - }, - Object { - "x": 1593413229000, - "y": 0, - }, - Object { - "x": 1593413230000, - "y": 0, - }, - Object { - "x": 1593413231000, - "y": 0, - }, - Object { - "x": 1593413232000, - "y": 0, - }, - Object { - "x": 1593413233000, - "y": 0, - }, - Object { - "x": 1593413234000, - "y": 0, - }, - Object { - "x": 1593413235000, - "y": 0, - }, - Object { - "x": 1593413236000, - "y": 0, - }, - Object { - "x": 1593413237000, - "y": 0, - }, - Object { - "x": 1593413238000, - "y": 0, - }, - Object { - "x": 1593413239000, - "y": 0, - }, - Object { - "x": 1593413240000, - "y": 0, - }, - Object { - "x": 1593413241000, - "y": 0, - }, - Object { - "x": 1593413242000, - "y": 0, - }, - Object { - "x": 1593413243000, - "y": 0, - }, - Object { - "x": 1593413244000, - "y": 0, - }, - Object { - "x": 1593413245000, - "y": 0, - }, - Object { - "x": 1593413246000, - "y": 0, - }, - Object { - "x": 1593413247000, - "y": 0, - }, - Object { - "x": 1593413248000, - "y": 0, - }, - Object { - "x": 1593413249000, - "y": 0, - }, - Object { - "x": 1593413250000, - "y": 0, - }, - Object { - "x": 1593413251000, - "y": 0, - }, - Object { - "x": 1593413252000, - "y": 0, - }, - Object { - "x": 1593413253000, - "y": 0, - }, - Object { - "x": 1593413254000, - "y": 0, - }, - Object { - "x": 1593413255000, - "y": 0, - }, - Object { - "x": 1593413256000, - "y": 0, - }, - Object { - "x": 1593413257000, - "y": 0, - }, - Object { - "x": 1593413258000, - "y": 0, - }, - Object { - "x": 1593413259000, - "y": 0, - }, - Object { - "x": 1593413260000, - "y": 0, - }, - Object { - "x": 1593413261000, - "y": 0, - }, - Object { - "x": 1593413262000, - "y": 0, - }, - Object { - "x": 1593413263000, - "y": 0, - }, - Object { - "x": 1593413264000, - "y": 0, - }, - Object { - "x": 1593413265000, - "y": 0, - }, - Object { - "x": 1593413266000, - "y": 0, - }, - Object { - "x": 1593413267000, - "y": 0, - }, - Object { - "x": 1593413268000, - "y": 0, - }, - Object { - "x": 1593413269000, - "y": 0, - }, - Object { - "x": 1593413270000, - "y": 0, - }, - Object { - "x": 1593413271000, - "y": 0, - }, - Object { - "x": 1593413272000, - "y": 0, - }, - Object { - "x": 1593413273000, - "y": 0, - }, - Object { - "x": 1593413274000, - "y": 0, - }, - Object { - "x": 1593413275000, - "y": 0, - }, - Object { - "x": 1593413276000, - "y": 0, - }, - Object { - "x": 1593413277000, - "y": 0, - }, - Object { - "x": 1593413278000, - "y": 0, - }, - Object { - "x": 1593413279000, - "y": 0, - }, - Object { - "x": 1593413280000, - "y": 0, - }, - Object { - "x": 1593413281000, - "y": 0, - }, - Object { - "x": 1593413282000, - "y": 0, - }, - Object { - "x": 1593413283000, - "y": 0, - }, - Object { - "x": 1593413284000, - "y": 0, - }, - Object { - "x": 1593413285000, - "y": 0, - }, - Object { - "x": 1593413286000, - "y": 0, - }, - Object { - "x": 1593413287000, - "y": 0, - }, - Object { - "x": 1593413288000, - "y": 0, - }, - Object { - "x": 1593413289000, - "y": 1, - }, - Object { - "x": 1593413290000, - "y": 0, - }, - Object { - "x": 1593413291000, - "y": 0, - }, - Object { - "x": 1593413292000, - "y": 1, - }, - Object { - "x": 1593413293000, - "y": 0, - }, - Object { - "x": 1593413294000, - "y": 0, - }, - Object { - "x": 1593413295000, - "y": 0, - }, - Object { - "x": 1593413296000, - "y": 0, - }, - Object { - "x": 1593413297000, - "y": 0, - }, - Object { - "x": 1593413298000, - "y": 0, - }, - Object { - "x": 1593413299000, - "y": 0, - }, - Object { - "x": 1593413300000, - "y": 1, - }, - Object { - "x": 1593413301000, - "y": 0, - }, - Object { - "x": 1593413302000, - "y": 0, - }, - Object { - "x": 1593413303000, - "y": 0, - }, - Object { - "x": 1593413304000, - "y": 0, - }, - Object { - "x": 1593413305000, - "y": 1, - }, - Object { - "x": 1593413306000, - "y": 0, - }, - Object { - "x": 1593413307000, - "y": 0, - }, - Object { - "x": 1593413308000, - "y": 0, - }, - Object { - "x": 1593413309000, - "y": 1, - }, - Object { - "x": 1593413310000, - "y": 1, - }, - Object { - "x": 1593413311000, - "y": 0, - }, - Object { - "x": 1593413312000, - "y": 0, - }, - Object { - "x": 1593413313000, - "y": 0, - }, - Object { - "x": 1593413314000, - "y": 0, - }, - Object { - "x": 1593413315000, - "y": 1, - }, - Object { - "x": 1593413316000, - "y": 0, - }, - Object { - "x": 1593413317000, - "y": 0, - }, - Object { - "x": 1593413318000, - "y": 0, - }, + "x": 1599721110000, + "y": 927744, + }, + Object { + "x": 1599721140000, + "y": 1048320, + }, + Object { + "x": 1599721170000, + "y": 1363968, + }, + Object { + "x": 1599721200000, + "y": null, + }, + ], + }, + "tpmBuckets": Array [ + Object { + "avg": 3.183333333333333, + "dataPoints": Array [ Object { - "x": 1593413319000, + "x": 1599717600000, "y": 0, }, Object { - "x": 1593413320000, + "x": 1599717630000, "y": 1, }, Object { - "x": 1593413321000, - "y": 0, - }, - Object { - "x": 1593413322000, - "y": 0, - }, - Object { - "x": 1593413323000, - "y": 0, - }, - Object { - "x": 1593413324000, - "y": 0, - }, - Object { - "x": 1593413325000, - "y": 0, - }, - Object { - "x": 1593413326000, - "y": 0, - }, - Object { - "x": 1593413327000, - "y": 0, - }, - Object { - "x": 1593413328000, - "y": 0, - }, - Object { - "x": 1593413329000, - "y": 0, - }, - Object { - "x": 1593413330000, - "y": 0, - }, - Object { - "x": 1593413331000, - "y": 0, - }, - Object { - "x": 1593413332000, - "y": 0, - }, - Object { - "x": 1593413333000, - "y": 0, + "x": 1599717660000, + "y": 2, }, Object { - "x": 1593413334000, + "x": 1599717690000, "y": 0, }, Object { - "x": 1593413335000, - "y": 0, + "x": 1599717720000, + "y": 3, }, Object { - "x": 1593413336000, - "y": 0, + "x": 1599717750000, + "y": 3, }, Object { - "x": 1593413337000, - "y": 0, + "x": 1599717780000, + "y": 2, }, Object { - "x": 1593413338000, + "x": 1599717810000, "y": 0, }, Object { - "x": 1593413339000, - "y": 0, + "x": 1599717840000, + "y": 1, }, Object { - "x": 1593413340000, - "y": 0, + "x": 1599717870000, + "y": 2, }, - ], - "key": "HTTP 4xx", - }, - Object { - "avg": 2.25, - "dataPoints": Array [ Object { - "x": 1593413100000, - "y": 0, + "x": 1599717900000, + "y": 2, }, Object { - "x": 1593413101000, + "x": 1599717930000, "y": 0, }, Object { - "x": 1593413102000, - "y": 0, + "x": 1599717960000, + "y": 4, }, Object { - "x": 1593413103000, - "y": 0, + "x": 1599717990000, + "y": 4, }, Object { - "x": 1593413104000, - "y": 0, + "x": 1599718020000, + "y": 1, }, Object { - "x": 1593413105000, + "x": 1599718050000, "y": 0, }, Object { - "x": 1593413106000, - "y": 0, + "x": 1599718080000, + "y": 5, }, Object { - "x": 1593413107000, - "y": 0, + "x": 1599718110000, + "y": 1, }, Object { - "x": 1593413108000, - "y": 0, + "x": 1599718140000, + "y": 2, }, Object { - "x": 1593413109000, + "x": 1599718170000, "y": 0, }, Object { - "x": 1593413110000, - "y": 0, + "x": 1599718200000, + "y": 6, }, Object { - "x": 1593413111000, - "y": 0, + "x": 1599718230000, + "y": 2, }, Object { - "x": 1593413112000, + "x": 1599718260000, "y": 0, }, Object { - "x": 1593413113000, + "x": 1599718290000, "y": 0, }, Object { - "x": 1593413114000, - "y": 0, + "x": 1599718320000, + "y": 2, }, Object { - "x": 1593413115000, - "y": 0, + "x": 1599718350000, + "y": 3, }, Object { - "x": 1593413116000, - "y": 0, + "x": 1599718380000, + "y": 2, }, Object { - "x": 1593413117000, + "x": 1599718410000, "y": 0, }, Object { - "x": 1593413118000, - "y": 0, + "x": 1599718440000, + "y": 3, }, Object { - "x": 1593413119000, - "y": 0, + "x": 1599718470000, + "y": 6, }, Object { - "x": 1593413120000, - "y": 0, + "x": 1599718500000, + "y": 1, }, Object { - "x": 1593413121000, + "x": 1599718530000, "y": 0, }, Object { - "x": 1593413122000, - "y": 0, + "x": 1599718560000, + "y": 7, }, Object { - "x": 1593413123000, + "x": 1599718590000, "y": 0, }, Object { - "x": 1593413124000, - "y": 0, + "x": 1599718620000, + "y": 2, }, Object { - "x": 1593413125000, + "x": 1599718650000, "y": 0, }, Object { - "x": 1593413126000, - "y": 0, + "x": 1599718680000, + "y": 3, }, Object { - "x": 1593413127000, - "y": 0, + "x": 1599718710000, + "y": 2, }, Object { - "x": 1593413128000, - "y": 0, + "x": 1599718740000, + "y": 1, }, Object { - "x": 1593413129000, - "y": 0, + "x": 1599718770000, + "y": 1, }, Object { - "x": 1593413130000, + "x": 1599718800000, "y": 0, }, Object { - "x": 1593413131000, - "y": 0, + "x": 1599718830000, + "y": 4, }, Object { - "x": 1593413132000, - "y": 0, + "x": 1599718860000, + "y": 1, }, Object { - "x": 1593413133000, + "x": 1599718890000, "y": 0, }, Object { - "x": 1593413134000, - "y": 0, + "x": 1599718920000, + "y": 2, }, Object { - "x": 1593413135000, + "x": 1599718950000, "y": 0, }, Object { - "x": 1593413136000, + "x": 1599718980000, "y": 0, }, Object { - "x": 1593413137000, - "y": 0, + "x": 1599719010000, + "y": 3, }, Object { - "x": 1593413138000, - "y": 0, + "x": 1599719040000, + "y": 4, }, Object { - "x": 1593413139000, - "y": 0, + "x": 1599719070000, + "y": 1, }, Object { - "x": 1593413140000, - "y": 0, + "x": 1599719100000, + "y": 2, }, Object { - "x": 1593413141000, + "x": 1599719130000, "y": 0, }, Object { - "x": 1593413142000, - "y": 0, + "x": 1599719160000, + "y": 6, }, Object { - "x": 1593413143000, - "y": 0, + "x": 1599719190000, + "y": 1, }, Object { - "x": 1593413144000, - "y": 0, + "x": 1599719220000, + "y": 5, }, Object { - "x": 1593413145000, + "x": 1599719250000, "y": 0, }, Object { - "x": 1593413146000, - "y": 0, + "x": 1599719280000, + "y": 2, }, Object { - "x": 1593413147000, - "y": 0, + "x": 1599719310000, + "y": 3, }, Object { - "x": 1593413148000, + "x": 1599719340000, "y": 0, }, Object { - "x": 1593413149000, + "x": 1599719370000, "y": 0, }, Object { - "x": 1593413150000, - "y": 0, + "x": 1599719400000, + "y": 6, }, Object { - "x": 1593413151000, - "y": 0, + "x": 1599719430000, + "y": 3, }, Object { - "x": 1593413152000, + "x": 1599719460000, "y": 0, }, Object { - "x": 1593413153000, - "y": 0, + "x": 1599719490000, + "y": 1, }, Object { - "x": 1593413154000, - "y": 0, + "x": 1599719520000, + "y": 1, }, Object { - "x": 1593413155000, + "x": 1599719550000, "y": 0, }, Object { - "x": 1593413156000, - "y": 0, + "x": 1599719580000, + "y": 1, }, Object { - "x": 1593413157000, + "x": 1599719610000, "y": 0, }, Object { - "x": 1593413158000, - "y": 0, + "x": 1599719640000, + "y": 4, }, Object { - "x": 1593413159000, - "y": 0, + "x": 1599719670000, + "y": 2, }, Object { - "x": 1593413160000, + "x": 1599719700000, "y": 0, }, Object { - "x": 1593413161000, - "y": 0, + "x": 1599719730000, + "y": 2, }, Object { - "x": 1593413162000, - "y": 0, + "x": 1599719760000, + "y": 3, }, Object { - "x": 1593413163000, + "x": 1599719790000, "y": 0, }, Object { - "x": 1593413164000, - "y": 0, + "x": 1599719820000, + "y": 2, }, Object { - "x": 1593413165000, + "x": 1599719850000, "y": 0, }, Object { - "x": 1593413166000, - "y": 0, + "x": 1599719880000, + "y": 4, }, Object { - "x": 1593413167000, + "x": 1599719910000, "y": 0, }, Object { - "x": 1593413168000, + "x": 1599719940000, "y": 0, }, Object { - "x": 1593413169000, - "y": 0, + "x": 1599719970000, + "y": 1, }, Object { - "x": 1593413170000, - "y": 0, + "x": 1599720000000, + "y": 1, }, Object { - "x": 1593413171000, - "y": 0, + "x": 1599720030000, + "y": 1, }, Object { - "x": 1593413172000, - "y": 0, + "x": 1599720060000, + "y": 1, }, Object { - "x": 1593413173000, + "x": 1599720090000, "y": 0, }, Object { - "x": 1593413174000, + "x": 1599720120000, "y": 0, }, Object { - "x": 1593413175000, + "x": 1599720150000, "y": 0, }, Object { - "x": 1593413176000, - "y": 0, + "x": 1599720180000, + "y": 1, }, Object { - "x": 1593413177000, + "x": 1599720210000, "y": 0, }, Object { - "x": 1593413178000, - "y": 0, + "x": 1599720240000, + "y": 4, }, Object { - "x": 1593413179000, - "y": 0, + "x": 1599720270000, + "y": 1, }, Object { - "x": 1593413180000, - "y": 0, + "x": 1599720300000, + "y": 2, }, Object { - "x": 1593413181000, + "x": 1599720330000, "y": 0, }, Object { - "x": 1593413182000, - "y": 0, + "x": 1599720360000, + "y": 1, }, Object { - "x": 1593413183000, - "y": 0, + "x": 1599720390000, + "y": 5, }, Object { - "x": 1593413184000, - "y": 0, + "x": 1599720420000, + "y": 4, }, Object { - "x": 1593413185000, + "x": 1599720450000, "y": 0, }, Object { - "x": 1593413186000, + "x": 1599720480000, "y": 0, }, Object { - "x": 1593413187000, - "y": 0, + "x": 1599720510000, + "y": 3, }, Object { - "x": 1593413188000, + "x": 1599720540000, "y": 0, }, Object { - "x": 1593413189000, + "x": 1599720570000, "y": 0, }, Object { - "x": 1593413190000, - "y": 0, + "x": 1599720600000, + "y": 2, }, Object { - "x": 1593413191000, - "y": 0, + "x": 1599720630000, + "y": 3, }, Object { - "x": 1593413192000, - "y": 0, + "x": 1599720660000, + "y": 1, }, Object { - "x": 1593413193000, + "x": 1599720690000, "y": 0, }, Object { - "x": 1593413194000, - "y": 0, + "x": 1599720720000, + "y": 2, }, Object { - "x": 1593413195000, - "y": 0, + "x": 1599720750000, + "y": 4, }, Object { - "x": 1593413196000, - "y": 0, + "x": 1599720780000, + "y": 2, }, Object { - "x": 1593413197000, + "x": 1599720810000, "y": 0, }, Object { - "x": 1593413198000, - "y": 0, + "x": 1599720840000, + "y": 1, }, Object { - "x": 1593413199000, - "y": 0, + "x": 1599720870000, + "y": 3, }, Object { - "x": 1593413200000, - "y": 0, + "x": 1599720900000, + "y": 3, }, Object { - "x": 1593413201000, + "x": 1599720930000, "y": 0, }, Object { - "x": 1593413202000, - "y": 0, + "x": 1599720960000, + "y": 1, }, Object { - "x": 1593413203000, - "y": 0, + "x": 1599720990000, + "y": 1, }, Object { - "x": 1593413204000, - "y": 0, + "x": 1599721020000, + "y": 1, }, Object { - "x": 1593413205000, + "x": 1599721050000, "y": 0, }, Object { - "x": 1593413206000, - "y": 0, + "x": 1599721080000, + "y": 4, }, Object { - "x": 1593413207000, + "x": 1599721110000, "y": 0, }, Object { - "x": 1593413208000, - "y": 0, + "x": 1599721140000, + "y": 1, }, Object { - "x": 1593413209000, + "x": 1599721170000, "y": 0, }, Object { - "x": 1593413210000, + "x": 1599721200000, "y": 0, }, + ], + "key": "HTTP 2xx", + }, + Object { + "avg": 0.21666666666666667, + "dataPoints": Array [ Object { - "x": 1593413211000, + "x": 1599717600000, "y": 0, }, Object { - "x": 1593413212000, + "x": 1599717630000, "y": 0, }, Object { - "x": 1593413213000, + "x": 1599717660000, "y": 0, }, Object { - "x": 1593413214000, + "x": 1599717690000, "y": 0, }, Object { - "x": 1593413215000, - "y": 0, + "x": 1599717720000, + "y": 1, }, Object { - "x": 1593413216000, + "x": 1599717750000, "y": 0, }, Object { - "x": 1593413217000, + "x": 1599717780000, "y": 0, }, Object { - "x": 1593413218000, + "x": 1599717810000, "y": 0, }, Object { - "x": 1593413219000, + "x": 1599717840000, "y": 0, }, Object { - "x": 1593413220000, + "x": 1599717870000, "y": 0, }, Object { - "x": 1593413221000, + "x": 1599717900000, "y": 0, }, Object { - "x": 1593413222000, + "x": 1599717930000, "y": 0, }, Object { - "x": 1593413223000, + "x": 1599717960000, "y": 0, }, Object { - "x": 1593413224000, + "x": 1599717990000, "y": 0, }, Object { - "x": 1593413225000, + "x": 1599718020000, "y": 0, }, Object { - "x": 1593413226000, + "x": 1599718050000, "y": 0, }, Object { - "x": 1593413227000, + "x": 1599718080000, "y": 0, }, Object { - "x": 1593413228000, + "x": 1599718110000, "y": 0, }, Object { - "x": 1593413229000, + "x": 1599718140000, "y": 0, }, Object { - "x": 1593413230000, + "x": 1599718170000, "y": 0, }, Object { - "x": 1593413231000, - "y": 0, + "x": 1599718200000, + "y": 1, }, Object { - "x": 1593413232000, + "x": 1599718230000, "y": 0, }, Object { - "x": 1593413233000, + "x": 1599718260000, "y": 0, }, Object { - "x": 1593413234000, + "x": 1599718290000, "y": 0, }, Object { - "x": 1593413235000, + "x": 1599718320000, "y": 0, }, Object { - "x": 1593413236000, + "x": 1599718350000, "y": 0, }, Object { - "x": 1593413237000, + "x": 1599718380000, "y": 0, }, Object { - "x": 1593413238000, + "x": 1599718410000, "y": 0, }, Object { - "x": 1593413239000, + "x": 1599718440000, "y": 0, }, Object { - "x": 1593413240000, - "y": 0, + "x": 1599718470000, + "y": 1, }, Object { - "x": 1593413241000, + "x": 1599718500000, "y": 0, }, Object { - "x": 1593413242000, + "x": 1599718530000, "y": 0, }, Object { - "x": 1593413243000, - "y": 0, + "x": 1599718560000, + "y": 2, }, Object { - "x": 1593413244000, + "x": 1599718590000, "y": 0, }, Object { - "x": 1593413245000, + "x": 1599718620000, "y": 0, }, Object { - "x": 1593413246000, + "x": 1599718650000, "y": 0, }, Object { - "x": 1593413247000, - "y": 0, + "x": 1599718680000, + "y": 2, }, Object { - "x": 1593413248000, + "x": 1599718710000, "y": 0, }, Object { - "x": 1593413249000, + "x": 1599718740000, "y": 0, }, Object { - "x": 1593413250000, + "x": 1599718770000, "y": 0, }, Object { - "x": 1593413251000, + "x": 1599718800000, "y": 0, }, Object { - "x": 1593413252000, + "x": 1599718830000, "y": 0, }, Object { - "x": 1593413253000, + "x": 1599718860000, "y": 0, }, Object { - "x": 1593413254000, + "x": 1599718890000, "y": 0, }, Object { - "x": 1593413255000, + "x": 1599718920000, "y": 0, }, Object { - "x": 1593413256000, + "x": 1599718950000, "y": 0, }, Object { - "x": 1593413257000, + "x": 1599718980000, "y": 0, }, Object { - "x": 1593413258000, + "x": 1599719010000, "y": 0, }, Object { - "x": 1593413259000, - "y": 0, + "x": 1599719040000, + "y": 1, }, Object { - "x": 1593413260000, + "x": 1599719070000, "y": 0, }, Object { - "x": 1593413261000, + "x": 1599719100000, "y": 0, }, Object { - "x": 1593413262000, + "x": 1599719130000, "y": 0, }, Object { - "x": 1593413263000, + "x": 1599719160000, "y": 0, }, Object { - "x": 1593413264000, + "x": 1599719190000, "y": 0, }, Object { - "x": 1593413265000, + "x": 1599719220000, "y": 0, }, Object { - "x": 1593413266000, + "x": 1599719250000, "y": 0, }, Object { - "x": 1593413267000, + "x": 1599719280000, "y": 0, }, Object { - "x": 1593413268000, + "x": 1599719310000, "y": 0, }, Object { - "x": 1593413269000, + "x": 1599719340000, "y": 0, }, Object { - "x": 1593413270000, + "x": 1599719370000, "y": 0, }, Object { - "x": 1593413271000, - "y": 0, + "x": 1599719400000, + "y": 2, }, Object { - "x": 1593413272000, + "x": 1599719430000, "y": 0, }, Object { - "x": 1593413273000, + "x": 1599719460000, "y": 0, }, Object { - "x": 1593413274000, + "x": 1599719490000, "y": 0, }, Object { - "x": 1593413275000, + "x": 1599719520000, "y": 0, }, Object { - "x": 1593413276000, + "x": 1599719550000, "y": 0, }, Object { - "x": 1593413277000, + "x": 1599719580000, "y": 0, }, Object { - "x": 1593413278000, + "x": 1599719610000, "y": 0, }, Object { - "x": 1593413279000, - "y": 0, + "x": 1599719640000, + "y": 1, }, Object { - "x": 1593413280000, - "y": 0, + "x": 1599719670000, + "y": 1, }, Object { - "x": 1593413281000, + "x": 1599719700000, "y": 0, }, Object { - "x": 1593413282000, + "x": 1599719730000, "y": 0, }, Object { - "x": 1593413283000, + "x": 1599719760000, "y": 0, }, Object { - "x": 1593413284000, + "x": 1599719790000, "y": 0, }, Object { - "x": 1593413285000, + "x": 1599719820000, "y": 0, }, Object { - "x": 1593413286000, - "y": 1, - }, - Object { - "x": 1593413287000, - "y": 1, - }, - Object { - "x": 1593413288000, + "x": 1599719850000, "y": 0, }, Object { - "x": 1593413289000, + "x": 1599719880000, "y": 0, }, Object { - "x": 1593413290000, + "x": 1599719910000, "y": 0, }, Object { - "x": 1593413291000, + "x": 1599719940000, "y": 0, }, Object { - "x": 1593413292000, + "x": 1599719970000, "y": 0, }, Object { - "x": 1593413293000, + "x": 1599720000000, "y": 0, }, Object { - "x": 1593413294000, + "x": 1599720030000, "y": 0, }, Object { - "x": 1593413295000, + "x": 1599720060000, "y": 0, }, Object { - "x": 1593413296000, + "x": 1599720090000, "y": 0, }, Object { - "x": 1593413297000, + "x": 1599720120000, "y": 0, }, Object { - "x": 1593413298000, + "x": 1599720150000, "y": 0, }, Object { - "x": 1593413299000, - "y": 1, - }, - Object { - "x": 1593413300000, + "x": 1599720180000, "y": 0, }, Object { - "x": 1593413301000, - "y": 1, - }, - Object { - "x": 1593413302000, + "x": 1599720210000, "y": 0, }, Object { - "x": 1593413303000, + "x": 1599720240000, "y": 0, }, Object { - "x": 1593413304000, + "x": 1599720270000, "y": 0, }, Object { - "x": 1593413305000, - "y": 1, - }, - Object { - "x": 1593413306000, + "x": 1599720300000, "y": 0, }, Object { - "x": 1593413307000, + "x": 1599720330000, "y": 0, }, Object { - "x": 1593413308000, - "y": 1, - }, - Object { - "x": 1593413309000, + "x": 1599720360000, "y": 0, }, Object { - "x": 1593413310000, + "x": 1599720390000, "y": 0, }, Object { - "x": 1593413311000, - "y": 1, - }, - Object { - "x": 1593413312000, + "x": 1599720420000, "y": 0, }, Object { - "x": 1593413313000, + "x": 1599720450000, "y": 0, }, Object { - "x": 1593413314000, + "x": 1599720480000, "y": 0, }, Object { - "x": 1593413315000, - "y": 1, - }, - Object { - "x": 1593413316000, + "x": 1599720510000, "y": 0, }, Object { - "x": 1593413317000, + "x": 1599720540000, "y": 0, }, Object { - "x": 1593413318000, + "x": 1599720570000, "y": 0, }, Object { - "x": 1593413319000, + "x": 1599720600000, "y": 0, }, Object { - "x": 1593413320000, + "x": 1599720630000, "y": 0, }, Object { - "x": 1593413321000, + "x": 1599720660000, "y": 0, }, Object { - "x": 1593413322000, - "y": 1, - }, - Object { - "x": 1593413323000, + "x": 1599720690000, "y": 0, }, Object { - "x": 1593413324000, + "x": 1599720720000, "y": 0, }, Object { - "x": 1593413325000, + "x": 1599720750000, "y": 0, }, Object { - "x": 1593413326000, + "x": 1599720780000, "y": 0, }, Object { - "x": 1593413327000, + "x": 1599720810000, "y": 0, }, Object { - "x": 1593413328000, + "x": 1599720840000, "y": 0, }, Object { - "x": 1593413329000, + "x": 1599720870000, "y": 0, }, Object { - "x": 1593413330000, + "x": 1599720900000, "y": 0, }, Object { - "x": 1593413331000, + "x": 1599720930000, "y": 0, }, Object { - "x": 1593413332000, - "y": 0, + "x": 1599720960000, + "y": 1, }, Object { - "x": 1593413333000, + "x": 1599720990000, "y": 0, }, Object { - "x": 1593413334000, + "x": 1599721020000, "y": 0, }, Object { - "x": 1593413335000, + "x": 1599721050000, "y": 0, }, Object { - "x": 1593413336000, + "x": 1599721080000, "y": 0, }, Object { - "x": 1593413337000, + "x": 1599721110000, "y": 0, }, Object { - "x": 1593413338000, + "x": 1599721140000, "y": 0, }, Object { - "x": 1593413339000, + "x": 1599721170000, "y": 0, }, Object { - "x": 1593413340000, + "x": 1599721200000, "y": 0, }, ], - "key": "HTTP 5xx", + "key": "HTTP 4xx", }, Object { - "avg": 0.25, + "avg": 0.11666666666666667, "dataPoints": Array [ Object { - "x": 1593413100000, - "y": 0, + "x": 1599717600000, + "y": 1, }, Object { - "x": 1593413101000, + "x": 1599717630000, "y": 0, }, Object { - "x": 1593413102000, + "x": 1599717660000, "y": 0, }, Object { - "x": 1593413103000, + "x": 1599717690000, "y": 0, }, Object { - "x": 1593413104000, + "x": 1599717720000, "y": 0, }, Object { - "x": 1593413105000, + "x": 1599717750000, "y": 0, }, Object { - "x": 1593413106000, + "x": 1599717780000, "y": 0, }, Object { - "x": 1593413107000, + "x": 1599717810000, "y": 0, }, Object { - "x": 1593413108000, + "x": 1599717840000, "y": 0, }, Object { - "x": 1593413109000, + "x": 1599717870000, "y": 0, }, Object { - "x": 1593413110000, + "x": 1599717900000, "y": 0, }, Object { - "x": 1593413111000, + "x": 1599717930000, "y": 0, }, Object { - "x": 1593413112000, + "x": 1599717960000, "y": 0, }, Object { - "x": 1593413113000, + "x": 1599717990000, "y": 0, }, Object { - "x": 1593413114000, + "x": 1599718020000, "y": 0, }, Object { - "x": 1593413115000, + "x": 1599718050000, "y": 0, }, Object { - "x": 1593413116000, + "x": 1599718080000, "y": 0, }, Object { - "x": 1593413117000, + "x": 1599718110000, "y": 0, }, Object { - "x": 1593413118000, + "x": 1599718140000, "y": 0, }, Object { - "x": 1593413119000, + "x": 1599718170000, "y": 0, }, Object { - "x": 1593413120000, + "x": 1599718200000, "y": 0, }, Object { - "x": 1593413121000, - "y": 0, + "x": 1599718230000, + "y": 1, }, Object { - "x": 1593413122000, + "x": 1599718260000, "y": 0, }, Object { - "x": 1593413123000, + "x": 1599718290000, "y": 0, }, Object { - "x": 1593413124000, + "x": 1599718320000, "y": 0, }, Object { - "x": 1593413125000, + "x": 1599718350000, "y": 0, }, Object { - "x": 1593413126000, + "x": 1599718380000, "y": 0, }, Object { - "x": 1593413127000, + "x": 1599718410000, "y": 0, }, Object { - "x": 1593413128000, + "x": 1599718440000, "y": 0, }, Object { - "x": 1593413129000, + "x": 1599718470000, "y": 0, }, Object { - "x": 1593413130000, + "x": 1599718500000, "y": 0, }, Object { - "x": 1593413131000, + "x": 1599718530000, "y": 0, }, Object { - "x": 1593413132000, + "x": 1599718560000, "y": 0, }, Object { - "x": 1593413133000, + "x": 1599718590000, "y": 0, }, Object { - "x": 1593413134000, + "x": 1599718620000, "y": 0, }, Object { - "x": 1593413135000, + "x": 1599718650000, "y": 0, }, Object { - "x": 1593413136000, + "x": 1599718680000, "y": 0, }, Object { - "x": 1593413137000, - "y": 0, + "x": 1599718710000, + "y": 1, }, Object { - "x": 1593413138000, + "x": 1599718740000, "y": 0, }, Object { - "x": 1593413139000, + "x": 1599718770000, "y": 0, }, Object { - "x": 1593413140000, + "x": 1599718800000, "y": 0, }, Object { - "x": 1593413141000, + "x": 1599718830000, "y": 0, }, Object { - "x": 1593413142000, - "y": 0, + "x": 1599718860000, + "y": 2, }, Object { - "x": 1593413143000, + "x": 1599718890000, "y": 0, }, Object { - "x": 1593413144000, + "x": 1599718920000, "y": 0, }, Object { - "x": 1593413145000, + "x": 1599718950000, "y": 0, }, Object { - "x": 1593413146000, + "x": 1599718980000, "y": 0, }, Object { - "x": 1593413147000, + "x": 1599719010000, "y": 0, }, Object { - "x": 1593413148000, + "x": 1599719040000, "y": 0, }, Object { - "x": 1593413149000, + "x": 1599719070000, "y": 0, }, Object { - "x": 1593413150000, + "x": 1599719100000, "y": 0, }, Object { - "x": 1593413151000, + "x": 1599719130000, "y": 0, }, Object { - "x": 1593413152000, + "x": 1599719160000, "y": 0, }, Object { - "x": 1593413153000, + "x": 1599719190000, "y": 0, }, Object { - "x": 1593413154000, + "x": 1599719220000, "y": 0, }, Object { - "x": 1593413155000, + "x": 1599719250000, "y": 0, }, Object { - "x": 1593413156000, + "x": 1599719280000, "y": 0, }, Object { - "x": 1593413157000, + "x": 1599719310000, "y": 0, }, Object { - "x": 1593413158000, + "x": 1599719340000, "y": 0, }, Object { - "x": 1593413159000, + "x": 1599719370000, "y": 0, }, Object { - "x": 1593413160000, + "x": 1599719400000, "y": 0, }, Object { - "x": 1593413161000, + "x": 1599719430000, "y": 0, }, Object { - "x": 1593413162000, + "x": 1599719460000, "y": 0, }, Object { - "x": 1593413163000, + "x": 1599719490000, "y": 0, }, Object { - "x": 1593413164000, + "x": 1599719520000, "y": 0, }, Object { - "x": 1593413165000, + "x": 1599719550000, "y": 0, }, Object { - "x": 1593413166000, + "x": 1599719580000, "y": 0, }, Object { - "x": 1593413167000, + "x": 1599719610000, "y": 0, }, Object { - "x": 1593413168000, + "x": 1599719640000, "y": 0, }, Object { - "x": 1593413169000, + "x": 1599719670000, "y": 0, }, Object { - "x": 1593413170000, + "x": 1599719700000, "y": 0, }, Object { - "x": 1593413171000, + "x": 1599719730000, "y": 0, }, Object { - "x": 1593413172000, + "x": 1599719760000, "y": 0, }, Object { - "x": 1593413173000, + "x": 1599719790000, "y": 0, }, Object { - "x": 1593413174000, + "x": 1599719820000, "y": 0, }, Object { - "x": 1593413175000, + "x": 1599719850000, "y": 0, }, Object { - "x": 1593413176000, + "x": 1599719880000, "y": 0, }, Object { - "x": 1593413177000, + "x": 1599719910000, "y": 0, }, Object { - "x": 1593413178000, + "x": 1599719940000, "y": 0, }, Object { - "x": 1593413179000, - "y": 0, + "x": 1599719970000, + "y": 1, }, Object { - "x": 1593413180000, + "x": 1599720000000, "y": 0, }, Object { - "x": 1593413181000, + "x": 1599720030000, "y": 0, }, Object { - "x": 1593413182000, + "x": 1599720060000, "y": 0, }, Object { - "x": 1593413183000, + "x": 1599720090000, "y": 0, }, Object { - "x": 1593413184000, + "x": 1599720120000, "y": 0, }, Object { - "x": 1593413185000, + "x": 1599720150000, "y": 0, }, Object { - "x": 1593413186000, + "x": 1599720180000, "y": 0, }, Object { - "x": 1593413187000, + "x": 1599720210000, "y": 0, }, Object { - "x": 1593413188000, + "x": 1599720240000, "y": 0, }, Object { - "x": 1593413189000, + "x": 1599720270000, "y": 0, }, Object { - "x": 1593413190000, + "x": 1599720300000, "y": 0, }, Object { - "x": 1593413191000, + "x": 1599720330000, "y": 0, }, Object { - "x": 1593413192000, + "x": 1599720360000, "y": 0, }, Object { - "x": 1593413193000, + "x": 1599720390000, "y": 0, }, Object { - "x": 1593413194000, + "x": 1599720420000, "y": 0, }, Object { - "x": 1593413195000, + "x": 1599720450000, "y": 0, }, Object { - "x": 1593413196000, + "x": 1599720480000, "y": 0, }, Object { - "x": 1593413197000, + "x": 1599720510000, "y": 0, }, Object { - "x": 1593413198000, + "x": 1599720540000, "y": 0, }, Object { - "x": 1593413199000, + "x": 1599720570000, "y": 0, }, Object { - "x": 1593413200000, + "x": 1599720600000, "y": 0, }, Object { - "x": 1593413201000, + "x": 1599720630000, "y": 0, }, Object { - "x": 1593413202000, + "x": 1599720660000, "y": 0, }, Object { - "x": 1593413203000, + "x": 1599720690000, "y": 0, }, Object { - "x": 1593413204000, + "x": 1599720720000, "y": 0, }, Object { - "x": 1593413205000, + "x": 1599720750000, "y": 0, }, Object { - "x": 1593413206000, + "x": 1599720780000, "y": 0, }, Object { - "x": 1593413207000, + "x": 1599720810000, "y": 0, }, Object { - "x": 1593413208000, + "x": 1599720840000, "y": 0, }, Object { - "x": 1593413209000, + "x": 1599720870000, "y": 0, }, Object { - "x": 1593413210000, + "x": 1599720900000, "y": 0, }, Object { - "x": 1593413211000, + "x": 1599720930000, "y": 0, }, Object { - "x": 1593413212000, + "x": 1599720960000, "y": 0, }, Object { - "x": 1593413213000, + "x": 1599720990000, "y": 0, }, Object { - "x": 1593413214000, + "x": 1599721020000, "y": 0, }, Object { - "x": 1593413215000, + "x": 1599721050000, "y": 0, }, Object { - "x": 1593413216000, - "y": 0, + "x": 1599721080000, + "y": 1, }, Object { - "x": 1593413217000, + "x": 1599721110000, "y": 0, }, Object { - "x": 1593413218000, + "x": 1599721140000, "y": 0, }, Object { - "x": 1593413219000, + "x": 1599721170000, "y": 0, }, Object { - "x": 1593413220000, + "x": 1599721200000, "y": 0, }, + ], + "key": "HTTP 5xx", + }, + Object { + "avg": 4.283333333333333, + "dataPoints": Array [ Object { - "x": 1593413221000, - "y": 0, + "x": 1599717600000, + "y": 2, }, Object { - "x": 1593413222000, - "y": 0, + "x": 1599717630000, + "y": 3, }, Object { - "x": 1593413223000, - "y": 0, + "x": 1599717660000, + "y": 1, }, Object { - "x": 1593413224000, - "y": 0, + "x": 1599717690000, + "y": 4, }, Object { - "x": 1593413225000, - "y": 0, + "x": 1599717720000, + "y": 3, }, Object { - "x": 1593413226000, - "y": 0, + "x": 1599717750000, + "y": 4, }, Object { - "x": 1593413227000, - "y": 0, + "x": 1599717780000, + "y": 2, }, Object { - "x": 1593413228000, - "y": 0, + "x": 1599717810000, + "y": 3, }, Object { - "x": 1593413229000, - "y": 0, + "x": 1599717840000, + "y": 2, }, Object { - "x": 1593413230000, - "y": 0, + "x": 1599717870000, + "y": 2, }, Object { - "x": 1593413231000, - "y": 0, + "x": 1599717900000, + "y": 1, }, Object { - "x": 1593413232000, - "y": 0, + "x": 1599717930000, + "y": 3, }, Object { - "x": 1593413233000, - "y": 0, + "x": 1599717960000, + "y": 1, }, Object { - "x": 1593413234000, - "y": 0, + "x": 1599717990000, + "y": 2, }, Object { - "x": 1593413235000, - "y": 0, + "x": 1599718020000, + "y": 3, }, Object { - "x": 1593413236000, - "y": 0, + "x": 1599718050000, + "y": 1, }, Object { - "x": 1593413237000, - "y": 0, + "x": 1599718080000, + "y": 2, }, Object { - "x": 1593413238000, - "y": 0, + "x": 1599718110000, + "y": 1, }, Object { - "x": 1593413239000, - "y": 0, + "x": 1599718140000, + "y": 3, }, Object { - "x": 1593413240000, - "y": 0, + "x": 1599718170000, + "y": 3, }, Object { - "x": 1593413241000, - "y": 0, + "x": 1599718200000, + "y": 2, }, Object { - "x": 1593413242000, - "y": 0, + "x": 1599718230000, + "y": 2, }, Object { - "x": 1593413243000, - "y": 0, + "x": 1599718260000, + "y": 2, }, Object { - "x": 1593413244000, - "y": 0, + "x": 1599718290000, + "y": 2, }, Object { - "x": 1593413245000, - "y": 0, + "x": 1599718320000, + "y": 2, }, Object { - "x": 1593413246000, - "y": 0, + "x": 1599718350000, + "y": 3, }, Object { - "x": 1593413247000, - "y": 0, + "x": 1599718380000, + "y": 3, }, Object { - "x": 1593413248000, - "y": 0, + "x": 1599718410000, + "y": 2, }, Object { - "x": 1593413249000, - "y": 0, + "x": 1599718440000, + "y": 2, }, Object { - "x": 1593413250000, - "y": 0, + "x": 1599718470000, + "y": 2, }, Object { - "x": 1593413251000, - "y": 0, + "x": 1599718500000, + "y": 2, }, Object { - "x": 1593413252000, - "y": 0, + "x": 1599718530000, + "y": 3, }, Object { - "x": 1593413253000, - "y": 0, + "x": 1599718560000, + "y": 1, }, Object { - "x": 1593413254000, - "y": 0, + "x": 1599718590000, + "y": 3, }, Object { - "x": 1593413255000, - "y": 0, + "x": 1599718620000, + "y": 3, }, Object { - "x": 1593413256000, - "y": 0, + "x": 1599718650000, + "y": 1, }, Object { - "x": 1593413257000, - "y": 0, + "x": 1599718680000, + "y": 3, }, Object { - "x": 1593413258000, - "y": 0, + "x": 1599718710000, + "y": 2, }, Object { - "x": 1593413259000, - "y": 0, + "x": 1599718740000, + "y": 1, }, Object { - "x": 1593413260000, - "y": 0, + "x": 1599718770000, + "y": 2, }, Object { - "x": 1593413261000, - "y": 0, + "x": 1599718800000, + "y": 2, }, Object { - "x": 1593413262000, - "y": 0, + "x": 1599718830000, + "y": 4, }, Object { - "x": 1593413263000, - "y": 0, + "x": 1599718860000, + "y": 2, }, Object { - "x": 1593413264000, - "y": 0, + "x": 1599718890000, + "y": 2, }, Object { - "x": 1593413265000, - "y": 0, + "x": 1599718920000, + "y": 1, }, Object { - "x": 1593413266000, - "y": 0, + "x": 1599718950000, + "y": 2, }, Object { - "x": 1593413267000, - "y": 0, + "x": 1599718980000, + "y": 3, }, Object { - "x": 1593413268000, - "y": 0, + "x": 1599719010000, + "y": 3, }, Object { - "x": 1593413269000, - "y": 0, + "x": 1599719040000, + "y": 1, }, Object { - "x": 1593413270000, - "y": 0, + "x": 1599719070000, + "y": 3, }, Object { - "x": 1593413271000, - "y": 0, + "x": 1599719100000, + "y": 2, }, Object { - "x": 1593413272000, - "y": 0, + "x": 1599719130000, + "y": 3, }, Object { - "x": 1593413273000, - "y": 0, + "x": 1599719160000, + "y": 1, }, Object { - "x": 1593413274000, - "y": 0, + "x": 1599719190000, + "y": 1, }, Object { - "x": 1593413275000, - "y": 0, + "x": 1599719220000, + "y": 4, }, Object { - "x": 1593413276000, + "x": 1599719250000, "y": 0, }, Object { - "x": 1593413277000, - "y": 0, + "x": 1599719280000, + "y": 4, }, Object { - "x": 1593413278000, + "x": 1599719310000, "y": 0, }, Object { - "x": 1593413279000, - "y": 0, + "x": 1599719340000, + "y": 2, }, Object { - "x": 1593413280000, - "y": 0, + "x": 1599719370000, + "y": 1, }, Object { - "x": 1593413281000, - "y": 0, + "x": 1599719400000, + "y": 4, }, Object { - "x": 1593413282000, - "y": 0, + "x": 1599719430000, + "y": 1, }, Object { - "x": 1593413283000, - "y": 0, + "x": 1599719460000, + "y": 3, }, Object { - "x": 1593413284000, - "y": 0, + "x": 1599719490000, + "y": 3, }, Object { - "x": 1593413285000, - "y": 0, + "x": 1599719520000, + "y": 2, }, Object { - "x": 1593413286000, - "y": 0, + "x": 1599719550000, + "y": 1, }, Object { - "x": 1593413287000, - "y": 0, + "x": 1599719580000, + "y": 4, }, Object { - "x": 1593413288000, - "y": 0, + "x": 1599719610000, + "y": 1, }, Object { - "x": 1593413289000, - "y": 0, + "x": 1599719640000, + "y": 2, }, Object { - "x": 1593413290000, - "y": 0, + "x": 1599719670000, + "y": 3, }, Object { - "x": 1593413291000, + "x": 1599719700000, "y": 0, }, Object { - "x": 1593413292000, - "y": 0, + "x": 1599719730000, + "y": 2, }, Object { - "x": 1593413293000, - "y": 0, + "x": 1599719760000, + "y": 4, }, Object { - "x": 1593413294000, - "y": 0, + "x": 1599719790000, + "y": 1, }, Object { - "x": 1593413295000, - "y": 0, + "x": 1599719820000, + "y": 2, }, Object { - "x": 1593413296000, - "y": 0, + "x": 1599719850000, + "y": 2, }, Object { - "x": 1593413297000, - "y": 0, + "x": 1599719880000, + "y": 2, }, Object { - "x": 1593413298000, - "y": 0, + "x": 1599719910000, + "y": 3, }, Object { - "x": 1593413299000, - "y": 0, + "x": 1599719940000, + "y": 3, }, Object { - "x": 1593413300000, - "y": 0, + "x": 1599719970000, + "y": 2, }, Object { - "x": 1593413301000, - "y": 0, + "x": 1599720000000, + "y": 2, }, Object { - "x": 1593413302000, - "y": 0, + "x": 1599720030000, + "y": 2, }, Object { - "x": 1593413303000, - "y": 0, + "x": 1599720060000, + "y": 1, }, Object { - "x": 1593413304000, - "y": 0, + "x": 1599720090000, + "y": 2, }, Object { - "x": 1593413305000, - "y": 0, + "x": 1599720120000, + "y": 1, }, Object { - "x": 1593413306000, - "y": 0, + "x": 1599720150000, + "y": 3, }, Object { - "x": 1593413307000, - "y": 0, + "x": 1599720180000, + "y": 2, }, Object { - "x": 1593413308000, - "y": 0, + "x": 1599720210000, + "y": 1, }, Object { - "x": 1593413309000, - "y": 1, + "x": 1599720240000, + "y": 3, }, Object { - "x": 1593413310000, - "y": 0, + "x": 1599720270000, + "y": 3, }, Object { - "x": 1593413311000, - "y": 0, + "x": 1599720300000, + "y": 2, }, Object { - "x": 1593413312000, - "y": 0, + "x": 1599720330000, + "y": 1, }, Object { - "x": 1593413313000, - "y": 0, + "x": 1599720360000, + "y": 2, }, Object { - "x": 1593413314000, - "y": 0, + "x": 1599720390000, + "y": 5, }, Object { - "x": 1593413315000, - "y": 0, + "x": 1599720420000, + "y": 2, }, Object { - "x": 1593413316000, - "y": 0, + "x": 1599720450000, + "y": 1, }, Object { - "x": 1593413317000, - "y": 0, + "x": 1599720480000, + "y": 3, }, Object { - "x": 1593413318000, - "y": 0, + "x": 1599720510000, + "y": 2, }, Object { - "x": 1593413319000, - "y": 0, + "x": 1599720540000, + "y": 2, }, Object { - "x": 1593413320000, - "y": 0, + "x": 1599720570000, + "y": 2, }, Object { - "x": 1593413321000, - "y": 0, + "x": 1599720600000, + "y": 1, }, Object { - "x": 1593413322000, - "y": 0, + "x": 1599720630000, + "y": 3, }, Object { - "x": 1593413323000, - "y": 0, + "x": 1599720660000, + "y": 2, }, Object { - "x": 1593413324000, - "y": 0, + "x": 1599720690000, + "y": 1, }, Object { - "x": 1593413325000, - "y": 0, + "x": 1599720720000, + "y": 2, }, Object { - "x": 1593413326000, - "y": 0, + "x": 1599720750000, + "y": 2, }, Object { - "x": 1593413327000, - "y": 0, + "x": 1599720780000, + "y": 2, }, Object { - "x": 1593413328000, - "y": 0, + "x": 1599720810000, + "y": 2, }, Object { - "x": 1593413329000, - "y": 0, + "x": 1599720840000, + "y": 2, }, Object { - "x": 1593413330000, - "y": 0, + "x": 1599720870000, + "y": 3, }, Object { - "x": 1593413331000, - "y": 0, + "x": 1599720900000, + "y": 1, }, Object { - "x": 1593413332000, - "y": 0, + "x": 1599720930000, + "y": 1, }, Object { - "x": 1593413333000, - "y": 0, + "x": 1599720960000, + "y": 3, }, Object { - "x": 1593413334000, - "y": 0, + "x": 1599720990000, + "y": 3, }, Object { - "x": 1593413335000, - "y": 0, + "x": 1599721020000, + "y": 1, }, Object { - "x": 1593413336000, - "y": 0, + "x": 1599721050000, + "y": 2, }, Object { - "x": 1593413337000, - "y": 0, + "x": 1599721080000, + "y": 2, }, Object { - "x": 1593413338000, - "y": 0, + "x": 1599721110000, + "y": 3, }, Object { - "x": 1593413339000, - "y": 0, + "x": 1599721140000, + "y": 2, + }, + Object { + "x": 1599721170000, + "y": 2, }, Object { - "x": 1593413340000, + "x": 1599721200000, "y": 0, }, ], diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts index 21f3aaa04a7b3..087bf1f0655e6 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -11,8 +12,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const transactionName = '/products'; const uiFilters = encodeURIComponent(JSON.stringify({})); @@ -28,23 +32,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the average duration by browser', async () => { const response = await supertest.get( - `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-rum/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` ); expect(response.status).to.be(200); + + expect(response.body.length).to.be.greaterThan(0); + expectSnapshot(response.body).toMatch(); + + expectSnapshot(response.body.length).toMatchInline(`1`); }); + it('returns the average duration by browser filtering by transaction name', async () => { const response = await supertest.get( - `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionName=${transactionName}` + `/api/apm/services/opbeans-rum/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionName=${transactionName}` ); expect(response.status).to.be(200); + + expect(response.body.length).to.be.greaterThan(0); + + expectSnapshot(response.body.length).toMatchInline(`1`); + expectSnapshot(response.body).toMatch(); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index 4e1b1e57fba0f..27e26bebd825b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -11,8 +12,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const transactionType = 'request'; const transactionName = 'GET /api'; const uiFilters = encodeURIComponent(JSON.stringify({})); @@ -29,8 +33,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the transaction breakdown for a service', async () => { const response = await supertest.get( @@ -46,45 +50,46 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); + const { timeseries } = response.body; + + const numberOfSeries = timeseries.length; + + expectSnapshot(numberOfSeries).toMatchInline(`1`); + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; - expectSnapshot(data).toMatchInline(` + const nonNullDataPoints = data.filter((y: number | null) => y !== null); + + expectSnapshot(nonNullDataPoints.length).toMatchInline(`121`); + + expectSnapshot( + data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { + return { + x: new Date(x ?? NaN).toISOString(), + y, + }; + }) + ).toMatchInline(` Array [ Object { - "x": 1593413100000, - "y": null, - }, - Object { - "x": 1593413130000, - "y": null, - }, - Object { - "x": 1593413160000, - "y": null, - }, - Object { - "x": 1593413190000, - "y": null, - }, - Object { - "x": 1593413220000, - "y": null, + "x": "2020-09-10T06:00:00.000Z", + "y": 1, }, Object { - "x": 1593413250000, - "y": null, + "x": "2020-09-10T06:00:30.000Z", + "y": 1, }, Object { - "x": 1593413280000, + "x": "2020-09-10T06:01:00.000Z", "y": null, }, Object { - "x": 1593413310000, + "x": "2020-09-10T06:01:30.000Z", "y": 1, }, Object { - "x": 1593413340000, + "x": "2020-09-10T06:02:00.000Z", "y": null, }, ] @@ -95,6 +100,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(type).toMatchInline(`"areaStacked"`); expectSnapshot(hideLegend).toMatchInline(`false`); expectSnapshot(legendValue).toMatchInline(`"100%"`); + + expectSnapshot(data).toMatch(); }); it('returns the transaction breakdown sorted by name', async () => { const response = await supertest.get( @@ -108,7 +115,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "app", "http", "postgresql", - "redis", ] `); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts index cf23883612b7c..c7e84bd5270ee 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; import { first, last } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -12,9 +13,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-08-26T11:00:00.000Z'); - const end = encodeURIComponent('2020-08-26T11:30:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({})); describe('Error rate', () => { @@ -24,16 +28,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/services/opbeans-java/transaction_groups/error_rate?start=${start}&end=${end}&uiFilters=${uiFilters}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ - noHits: true, - erroneousTransactionsRate: [], - average: null, - }); + + expect(response.body.noHits).to.be(true); + + expect(response.body.erroneousTransactionsRate.length).to.be(0); + expect(response.body.average).to.be(null); }); }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); describe('returns the transaction error rate', () => { let errorRateResponse: { @@ -50,26 +54,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has the correct start date', () => { expectSnapshot( new Date(first(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString() - ).toMatchInline(`"2020-08-26T11:00:00.000Z"`); + ).toMatchInline(`"2020-09-10T06:00:00.000Z"`); }); it('has the correct end date', () => { expectSnapshot( new Date(last(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString() - ).toMatchInline(`"2020-08-26T11:30:00.000Z"`); + ).toMatchInline(`"2020-09-10T07:00:00.000Z"`); }); it('has the correct number of buckets', () => { - expectSnapshot(errorRateResponse.erroneousTransactionsRate.length).toMatchInline(`61`); + expectSnapshot(errorRateResponse.erroneousTransactionsRate.length).toMatchInline(`121`); }); it('has the correct calculation for average', () => { - expectSnapshot(errorRateResponse.average).toMatchInline(`0.18894993894993897`); + expectSnapshot(errorRateResponse.average).toMatchInline(`0.16097046413502106`); }); it('has the correct error rate', () => { expectSnapshot(first(errorRateResponse.erroneousTransactionsRate)?.y).toMatchInline( - `0.5` + `0.6666666666666666` ); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts index cebf27ecdff2b..e944235ac41a8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -20,9 +21,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({})); const transactionType = 'request'; @@ -34,39 +38,37 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "bucketSize": 1000, - "isAggregationAccurate": true, - "items": Array [], - } - `); + + expect(response.body.isAggregationAccurate).to.be(true); + expect(response.body.items.length).to.be(0); }); }); describe('when data is loaded', () => { let response: any; before(async () => { - await esArchiver.load('8.0.0'); + await esArchiver.load(archiveName); response = await supertest.get( `/api/apm/services/opbeans-node/transaction_groups?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); }); - after(() => esArchiver.unload('8.0.0')); + after(() => esArchiver.unload(archiveName)); - it('returns the correct status code', async () => { + it('returns the correct metadata', () => { expect(response.status).to.be(200); + expect(response.body.isAggregationAccurate).to.be(true); + expect(response.body.items.length).to.be.greaterThan(0); }); - it('returns the correct number of buckets', async () => { - expectSnapshot(response.body.items.length).toMatchInline(`18`); + it('returns the correct number of buckets', () => { + expectSnapshot(response.body.items.length).toMatchInline(`13`); }); - it('returns the correct buckets (when ignoring samples)', async () => { + it('returns the correct buckets (when ignoring samples)', () => { expectSnapshot(omitSampleFromTransactionGroups(response.body.items)).toMatch(); }); - it('returns the correct buckets and samples', async () => { + it('returns the correct buckets and samples', () => { // sample should provide enough information to deeplink to a transaction detail page response.body.items.forEach((item: any) => { expect(item.sample.trace.id).to.be.an('string'); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index a8418fe2860a3..43504a4ec401e 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -11,9 +13,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({})); describe('Transaction charts', () => { @@ -24,32 +29,44 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "apmTimeseries": Object { - "overallAvgDuration": null, - "responseTimes": Object { - "avg": Array [], - "p95": Array [], - "p99": Array [], - }, - "tpmBuckets": Array [], - }, - } - `); + + expect(response.body.apmTimeseries.overallAvgDuration).to.be(null); + expect(response.body.apmTimeseries.responseTimes.avg.length).to.be(0); + expect(response.body.apmTimeseries.responseTimes.p95.length).to.be(0); + expect(response.body.apmTimeseries.responseTimes.p99.length).to.be(0); + expect(response.body.apmTimeseries.tpmBuckets.length).to.be(0); }); }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('returns the transaction charts', async () => { - const response = await supertest.get( + let response: PromiseReturnType; + + before(async () => { + response = await supertest.get( `/api/apm/services/opbeans-node/transaction_groups/charts?start=${start}&end=${end}&uiFilters=${uiFilters}` ); + }); + it('returns some data', async () => { expect(response.status).to.be(200); + + expect(response.body.apmTimeseries.overallAvgDuration).not.to.be(null); + expect(response.body.apmTimeseries.responseTimes.avg.length).to.be.greaterThan(0); + expect(response.body.apmTimeseries.responseTimes.p95.length).to.be.greaterThan(0); + expect(response.body.apmTimeseries.responseTimes.p99.length).to.be.greaterThan(0); + expect(response.body.apmTimeseries.tpmBuckets.length).to.be.greaterThan(0); + }); + + it('returns the correct data', () => { + expectSnapshot(response.body.apmTimeseries.overallAvgDuration).toMatchInline( + `578297.1431623931` + ); + expectSnapshot(response.body.apmTimeseries.responseTimes.avg.length).toMatchInline(`121`); + expectSnapshot(response.body.apmTimeseries.tpmBuckets.length).toMatchInline(`4`); + expectSnapshot(response.body).toMatch(); }); }); diff --git a/x-pack/test/apm_api_integration/common/match_snapshot.ts b/x-pack/test/apm_api_integration/common/match_snapshot.ts index a8cb0418583af..4ac812a0ee168 100644 --- a/x-pack/test/apm_api_integration/common/match_snapshot.ts +++ b/x-pack/test/apm_api_integration/common/match_snapshot.ts @@ -12,6 +12,7 @@ import prettier from 'prettier'; // @ts-expect-error import babelTraverse from '@babel/traverse'; import { Suite, Test } from 'mocha'; +import { flatten } from 'lodash'; type ISnapshotState = InstanceType; @@ -143,18 +144,24 @@ Error.prepareStackTrace = (error, structuredStackTrace) => { } }; +function recursivelyGetTestsFromSuite(suite: Suite): Test[] { + return suite.tests.concat(flatten(suite.suites.map((s) => recursivelyGetTestsFromSuite(s)))); +} + function getSnapshotState(file: string, test: Test) { const dirname = path.dirname(file); const filename = path.basename(file); - let parent = test.parent; - const testsInFile: Test[] = []; + let parent: Suite | undefined = test.parent; - while (parent) { - testsInFile.push(...parent.tests); + while (parent && parent.parent?.file === file) { parent = parent.parent; } + if (!parent) { + throw new Error('Top-level suite not found'); + } + const snapshotState = new SnapshotState( path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')), { @@ -164,7 +171,7 @@ function getSnapshotState(file: string, test: Test) { } ); - return { snapshotState, testsInFile }; + return { snapshotState, testsInFile: recursivelyGetTestsFromSuite(parent) }; } export function expectSnapshot(received: any) { diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 48ffa13012696..c5ca086b5f370 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -16,6 +16,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr describe('Services', function () { loadTestFile(require.resolve('./services/annotations')); loadTestFile(require.resolve('./services/rum_services.ts')); + loadTestFile(require.resolve('./services/top_services.ts')); }); describe('Settings', function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap new file mode 100644 index 0000000000000..859f928c211a4 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -0,0 +1,1393 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Service Maps with a trial license /api/apm/service-map when there is data returns the correct data 3`] = ` +Array [ + Object { + "data": Object { + "id": "opbeans-go~>postgresql", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-go~opbeans-node", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-go~opbeans-python", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-go~opbeans-ruby", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-java~>postgresql", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-node", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-python", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-ruby", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~>postgresql", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~opbeans-go", + "isInverseEdge": true, + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-node~opbeans-python", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>elasticsearch", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">elasticsearch", + "targetData": Object { + "id": ">elasticsearch", + "label": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.subtype": "elasticsearch", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>postgresql", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>redis", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">redis", + "targetData": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-go", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-node", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-python~opbeans-ruby", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~>postgresql", + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-node", + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-python", + "isInverseEdge": true, + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-go", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-java", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-node", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-python", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-ruby", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + }, + Object { + "data": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + Object { + "data": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + Object { + "data": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "data": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "data": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + Object { + "data": Object { + "id": ">elasticsearch", + "label": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.subtype": "elasticsearch", + "span.type": "db", + }, + }, + Object { + "data": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + Object { + "data": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "db", + }, + }, + Object { + "data": Object { + "agent.name": "dotnet", + "id": "opbeans-dotnet", + "service.environment": null, + "service.name": "opbeans-dotnet", + }, + }, +] +`; + +exports[`Service Maps with a trial license when there is data with anomalies returns the correct anomaly stats 3`] = ` +Object { + "elements": Array [ + Object { + "data": Object { + "id": "opbeans-go~>postgresql", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-go~opbeans-node", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-go~opbeans-python", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-go~opbeans-ruby", + "source": "opbeans-go", + "sourceData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-java~>postgresql", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-node", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-python", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-ruby", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~>postgresql", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~opbeans-go", + "isInverseEdge": true, + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-node~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-node~opbeans-python", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>elasticsearch", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">elasticsearch", + "targetData": Object { + "id": ">elasticsearch", + "label": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.subtype": "elasticsearch", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>postgresql", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~>redis", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": ">redis", + "targetData": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-go", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-python~opbeans-node", + "isInverseEdge": true, + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-python~opbeans-ruby", + "source": "opbeans-python", + "sourceData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~>postgresql", + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-node", + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-ruby~opbeans-python", + "isInverseEdge": true, + "source": "opbeans-ruby", + "sourceData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-go", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-go", + "targetData": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-java", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-node", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-python", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-python", + "targetData": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + }, + Object { + "data": Object { + "id": "opbeans-rum~opbeans-ruby", + "source": "opbeans-rum", + "sourceData": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + "target": "opbeans-ruby", + "targetData": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + }, + Object { + "data": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + }, + }, + Object { + "data": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + }, + }, + Object { + "data": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + }, + }, + Object { + "data": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "data": Object { + "agent.name": "ruby", + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "data": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + }, + Object { + "data": Object { + "id": ">elasticsearch", + "label": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.subtype": "elasticsearch", + "span.type": "db", + }, + }, + Object { + "data": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + Object { + "data": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "db", + }, + }, + Object { + "data": Object { + "agent.name": "dotnet", + "id": "opbeans-dotnet", + "service.environment": null, + "service.name": "opbeans-dotnet", + }, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index f799d80f6ef13..9dc13f58268df 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -6,7 +6,9 @@ import querystring from 'querystring'; import expect from '@kbn/expect'; -import { isEmpty } from 'lodash'; +import { isEmpty, uniq } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -14,13 +16,16 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); + describe('Service Maps with a trial license', () => { describe('/api/apm/service-map', () => { describe('when there is no data', () => { it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' - ); + const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); expect(response.status).to.be(200); expect(response.body.elements.length).to.be(0); @@ -28,239 +33,56 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); describe('when there is data', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('returns service map elements', async () => { - const response = await supertest.get( - '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' - ); + let response: PromiseReturnType; + before(async () => { + response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + }); + + it('returns service map elements', () => { expect(response.status).to.be(200); + expect(response.body.elements.length).to.be.greaterThan(0); + }); - expectSnapshot(response.body).toMatchInline(` - Object { - "elements": Array [ - Object { - "data": Object { - "id": "client~opbeans-node", - "source": "client", - "sourceData": Object { - "agent.name": "rum-js", - "id": "client", - "service.name": "client", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>opbeans-java:3000", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - }, - "target": ">opbeans-java:3000", - "targetData": Object { - "id": ">opbeans-java:3000", - "label": "opbeans-java:3000", - "span.destination.service.resource": "opbeans-java:3000", - "span.subtype": "http", - "span.type": "external", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>postgresql", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-node", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>93.184.216.34:80", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - "target": ">93.184.216.34:80", - "targetData": Object { - "id": ">93.184.216.34:80", - "label": "93.184.216.34:80", - "span.destination.service.resource": "93.184.216.34:80", - "span.subtype": "http", - "span.type": "external", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>postgresql", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>redis", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - "target": ">redis", - "targetData": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "cache", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "production", - "service.name": "opbeans-node", - }, - }, - Object { - "data": Object { - "id": ">opbeans-java:3000", - "label": "opbeans-java:3000", - "span.destination.service.resource": "opbeans-java:3000", - "span.subtype": "http", - "span.type": "external", - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "client", - "service.name": "client", - }, - }, - Object { - "data": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "cache", - }, - }, - Object { - "data": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - Object { - "data": Object { - "id": ">93.184.216.34:80", - "label": "93.184.216.34:80", - "span.destination.service.resource": "93.184.216.34:80", - "span.subtype": "http", - "span.type": "external", - }, - }, - ], - } + it('returns the correct data', () => { + const elements: Array<{ data: Record }> = response.body.elements; + + const serviceNames = uniq( + elements + .filter((element) => element.data['service.name'] !== undefined) + .map((element) => element.data['service.name']) + ); + + expectSnapshot(serviceNames).toMatchInline(` + Array [ + "opbeans-rum", + "opbeans-go", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-java", + "opbeans-dotnet", + ] + `); + + const externalDestinations = uniq( + elements + .filter((element) => element.data.target?.startsWith('>')) + .map((element) => element.data.target) + ); + + expectSnapshot(externalDestinations).toMatchInline(` + Array [ + ">postgresql", + ">elasticsearch", + ">redis", + ] `); + + expectSnapshot(elements).toMatch(); }); }); }); @@ -269,48 +91,75 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) describe('when there is no data', () => { it('returns an object with nulls', async () => { const q = querystring.stringify({ - start: '2020-06-28T10:24:46.055Z', - end: '2020-06-29T10:24:46.055Z', + start: metadata.start, + end: metadata.end, uiFilters: {}, }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); expect(response.status).to.be(200); - expect(response.body).to.eql({ - avgCpuUsage: null, - avgErrorRate: null, - avgMemoryUsage: null, - transactionStats: { - avgRequestsPerMinute: null, - avgTransactionDuration: null, - }, - }); + expect(response.body.avgCpuUsage).to.be(null); + expect(response.body.avgErrorRate).to.be(null); + expect(response.body.avgMemoryUsage).to.be(null); + expect(response.body.transactionStats.avgRequestsPerMinute).to.be(null); + expect(response.body.transactionStats.avgTransactionDuration).to.be(null); }); }); }); describe('when there is data with anomalies', () => { - before(() => esArchiver.load('apm_8.0.0')); - after(() => esArchiver.unload('apm_8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('returns service map elements', async () => { - const start = encodeURIComponent('2020-09-10T06:00:00.000Z'); - const end = encodeURIComponent('2020-09-10T07:00:00.000Z'); + let response: PromiseReturnType; - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + before(async () => { + response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + }); + it('returns service map elements with anomaly stats', () => { expect(response.status).to.be(200); const dataWithAnomalies = response.body.elements.filter( (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) ); + expect(dataWithAnomalies).to.not.empty(); + dataWithAnomalies.forEach(({ data }: any) => { expect( Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) ).to.not.empty(); }); }); + + it('returns the correct anomaly stats', () => { + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); + + expectSnapshot(dataWithAnomalies.length).toMatchInline(`1`); + expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(` + Array [ + Object { + "data": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 1707977.2499999995, + "anomalyScore": 0.12232533657975532, + "jobId": "apm-production-229a-high_mean_transaction_duration", + "transactionType": "request", + }, + }, + }, + ] + `); + + expectSnapshot(response.body).toMatch(); + }); }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts new file mode 100644 index 0000000000000..76af02ec1606e --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + + // url parameters + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('APM Services Overview', () => { + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); + + it('some items have severity set', () => { + // Under the assumption that the loaded archive has + // at least one APM ML job, and the time range is longer + // than 15m, at least one items should have severity set. + // Note that we currently have a bug where healthy services + // report as unknown (so without any severity status): + // https://github.com/elastic/kibana/issues/77083 + + const severityScores = response.body.items.map((item: any) => item.severity); + + expect(severityScores.filter(Boolean).length).to.be.greaterThan(0); + + expectSnapshot(severityScores).toMatchInline(` + Array [ + undefined, + undefined, + undefined, + undefined, + undefined, + "warning", + undefined, + ] + `); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index b6ccd68bb2096..a147b56d56251 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -5,12 +5,12 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; +import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform'; function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { - id: `ec_2_${date}`, + id: `ec_cloning_${date}`, source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, @@ -32,7 +32,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig); + await transform.api.createAndRunTransform(transformConfig.id, transformConfig); await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 4e2b832838b7d..13213679a6117 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; interface GroupByEntry { @@ -141,7 +143,7 @@ export default function ({ getService }: FtrProviderContext) { values: [`Men's Accessories`], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, @@ -239,7 +241,7 @@ export default function ({ getService }: FtrProviderContext) { values: ['AE', 'CO', 'EG', 'FR', 'GB'], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 229ff97782362..20d276c2e017b 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; interface GroupByEntry { @@ -58,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { values: ['ASA'], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 460e7c5b24a98..ac955bde4ad5d 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { - id: `ec_2_${date}`, + id: `ec_editing_${date}`, source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, @@ -32,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig); + await transform.api.createAndRunTransform(transformConfig.id, transformConfig); await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); @@ -52,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { messageText: 'updated transform.', row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/services/transform/api.ts b/x-pack/test/functional/services/transform/api.ts index 697020fafb196..d97db93c31b3b 100644 --- a/x-pack/test/functional/services/transform/api.ts +++ b/x-pack/test/functional/services/transform/api.ts @@ -5,13 +5,17 @@ */ import expect from '@kbn/expect'; +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import { TransformState, TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import type { TransformStats } from '../../../../plugins/transform/common/types/transform_stats'; + import { FtrProviderContext } from '../../ftr_provider_context'; -import { TRANSFORM_STATE } from '../../../../plugins/transform/common'; -import { - TransformPivotConfig, - TransformStats, -} from '../../../../plugins/transform/public/app/common'; +export async function asyncForEach(array: any[], callback: Function) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -35,7 +39,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { await this.waitForIndicesToExist(indices, `expected ${indices} to be created`); }, - async deleteIndices(indices: string) { + async deleteIndices(indices: string, skipWaitForIndicesNotToExist?: boolean) { log.debug(`Deleting indices: '${indices}'...`); if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { log.debug(`Indices '${indices}' don't exist. Nothing to delete.`); @@ -49,7 +53,13 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { .to.have.property('acknowledged') .eql(true, 'Response for delete request should be acknowledged'); - await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`); + // Check for the option to skip the check if the indices are deleted. + // For example, we might want to clear the .transform-* indices but they + // will be automatically regenerated making tests flaky without the option + // to skip this check. + if (!skipWaitForIndicesNotToExist) { + await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`); + } }, async waitForIndicesToExist(indices: string, errorMsg?: string) { @@ -73,7 +83,26 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { }, async cleanTransformIndices() { - await this.deleteIndices('.transform-*'); + // Delete all transforms using the API since we mustn't just delete + // all `.transform-*` indices since this might result in orphaned ES tasks. + const { + body: { transforms }, + } = await esSupertest.get(`/_transform/`).expect(200); + const transformIds = transforms.map((t: { id: string }) => t.id); + + await asyncForEach(transformIds, async (transformId: string) => { + await esSupertest + .post(`/_transform/${transformId}/_stop?force=true&wait_for_completion`) + .expect(200); + await this.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + + await esSupertest.delete(`/_transform/${transformId}`).expect(200); + await this.waitForTransformNotToExist(transformId); + }); + + // Delete all transform related notifications to clear messages tabs + // in the transforms list expanded rows. + await this.deleteIndices('.transform-notifications-*'); }, async getTransformStats(transformId: string): Promise { @@ -90,12 +119,12 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { return statsResponse.transforms[0]; }, - async getTransformState(transformId: string): Promise { + async getTransformState(transformId: string): Promise { const stats = await this.getTransformStats(transformId); return stats.state; }, - async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) { + async waitForTransformState(transformId: string, expectedState: TransformState) { await retry.waitForWithTimeout( `transform state to be ${expectedState}`, 2 * 60 * 1000, @@ -110,6 +139,23 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { ); }, + async waitForTransformStateNotToBe(transformId: string, notExpectedState: TransformState) { + await retry.waitForWithTimeout( + `transform state not to be ${notExpectedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getTransformState(transformId); + if (state !== notExpectedState) { + return true; + } else { + throw new Error( + `expected transform state to not be ${notExpectedState} but got ${state}` + ); + } + } + ); + }, + async waitForBatchTransformToComplete(transformId: string) { await retry.waitForWithTimeout(`batch transform to complete`, 2 * 60 * 1000, async () => { const stats = await this.getTransformStats(transformId); @@ -127,8 +173,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode); }, - async createTransform(transformConfig: TransformPivotConfig) { - const transformId = transformConfig.id; + async createTransform(transformId: string, transformConfig: PutTransformsRequestSchema) { log.debug(`Creating transform with id '${transformId}'...`); await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200); @@ -147,6 +192,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { } }); }, + async waitForTransformNotToExist(transformId: string, errorMsg?: string) { await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => { if (await this.getTransform(transformId, 404)) { @@ -162,15 +208,15 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); }, - async createAndRunTransform(transformConfig: TransformPivotConfig) { - await this.createTransform(transformConfig); - await this.startTransform(transformConfig.id); + async createAndRunTransform(transformId: string, transformConfig: PutTransformsRequestSchema) { + await this.createTransform(transformId, transformConfig); + await this.startTransform(transformId); if (transformConfig.sync === undefined) { // batch mode - await this.waitForBatchTransformToComplete(transformConfig.id); + await this.waitForBatchTransformToComplete(transformId); } else { // continuous mode - await this.waitForTransformState(transformConfig.id, TRANSFORM_STATE.STARTED); + await this.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED); } }, }; diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 77e52b642261b..cc360379f32c3 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -174,7 +174,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformMessagesTab'); await testSubjects.click('transformMessagesTab'); await testSubjects.existOrFail('~transformMessagesTabContent'); - await retry.tryForTime(5000, async () => { + await retry.tryForTime(30 * 1000, async () => { const actualText = await testSubjects.getVisibleText('~transformMessagesTabContent'); expect(actualText.includes(expectedText)).to.eql( true, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 8555814245909..28743ee5f43c2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -11,6 +11,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install_by_upload')); loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_by_upload.ts new file mode 100644 index 0000000000000..e6d2affaec0cd --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_by_upload.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import fs from 'fs'; +import path from 'path'; +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const log = getService('log'); + + const testPkgArchiveTgz = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.tar.gz' + ); + const testPkgKey = 'apache-0.14'; + const server = dockerServers.get('registry'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('installs packages from direct upload', async () => { + after(async () => { + if (server.enabled) { + // remove the package just in case it being installed will affect other tests + await deletePackage(testPkgKey); + } + }); + + it('should install a tar archive correctly', async function () { + if (server.enabled) { + const buf = fs.readFileSync(testPkgArchiveTgz); + const res = await supertest + .post(`/api/ingest_manager/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/gzip') + .send(buf) + .expect(200); + expect(res.body.response).to.equal( + 'package upload was received ok, but not installed (not implemented yet)' + ); + } else { + warnAndSkipTest(this, log); + } + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz b/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz new file mode 100644 index 0000000000000..cc983f6ac6d1a Binary files /dev/null and b/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz differ